kybernus 3.0.0 → 3.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 (110) hide show
  1. package/README.md +1 -1
  2. package/package.json +2 -2
  3. package/templates/java-spring/clean/.gitignore.hbs +72 -0
  4. package/templates/java-spring/clean/docker-compose.yml.hbs +6 -3
  5. package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +21 -17
  6. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/entity/UserEntity.java.hbs +52 -0
  7. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/repository/JpaUserRepository.java.hbs +12 -0
  8. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/JwtAuthenticationFilter.java.hbs +64 -0
  9. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/SecurityConfig.java.hbs +36 -0
  10. package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/stripe/StripeGateway.java.hbs +63 -0
  11. package/templates/java-spring/clean/src/main/resources/application.properties.hbs +6 -7
  12. package/templates/java-spring/hexagonal/.gitignore.hbs +72 -0
  13. package/templates/java-spring/hexagonal/docker-compose.yml.hbs +6 -3
  14. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/JwtFilter.java.hbs +71 -0
  15. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/SecurityConfig.java.hbs +35 -0
  16. package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +3 -3
  17. package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +4 -4
  18. package/templates/java-spring/mvc/.gitignore.hbs +72 -0
  19. package/templates/java-spring/mvc/docker-compose.yml.hbs +6 -3
  20. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/config/SecurityConfig.java.hbs +13 -12
  21. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/AuthController.java.hbs +9 -8
  22. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +5 -6
  23. package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +3 -3
  24. package/templates/java-spring/mvc/src/main/resources/application.yml.hbs +29 -26
  25. package/templates/nestjs/clean/.gitignore.hbs +42 -0
  26. package/templates/nestjs/clean/Dockerfile.hbs +6 -3
  27. package/templates/nestjs/clean/docker-compose.yml.hbs +1 -11
  28. package/templates/nestjs/clean/package.json.hbs +51 -41
  29. package/templates/nestjs/clean/src/app.module.ts.hbs +2 -1
  30. package/templates/nestjs/clean/src/application/payment.service.ts.hbs +72 -72
  31. package/templates/nestjs/clean/src/domain/entities/user.entity.ts.hbs +2 -2
  32. package/templates/nestjs/clean/src/domain/repositories/user.repository.ts.hbs +2 -2
  33. package/templates/nestjs/clean/src/infrastructure/database/repositories/prisma.user.repository.ts.hbs +18 -18
  34. package/templates/nestjs/clean/src/infrastructure/http/health.controller.ts.hbs +9 -0
  35. package/templates/nestjs/clean/src/main.ts.hbs +1 -4
  36. package/templates/nestjs/clean/src/payment.module.ts.hbs +12 -12
  37. package/templates/nestjs/hexagonal/.gitignore.hbs +42 -0
  38. package/templates/nestjs/hexagonal/Dockerfile.hbs +6 -3
  39. package/templates/nestjs/hexagonal/docker-compose.yml.hbs +1 -11
  40. package/templates/nestjs/hexagonal/package.json.hbs +51 -41
  41. package/templates/nestjs/hexagonal/src/adapters/inbound/health.controller.ts.hbs +9 -0
  42. package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -1
  43. package/templates/nestjs/hexagonal/src/core/domain/user.entity.ts.hbs +6 -6
  44. package/templates/nestjs/hexagonal/src/core/ports/ports.ts.hbs +4 -4
  45. package/templates/nestjs/hexagonal/src/main.ts.hbs +1 -4
  46. package/templates/nestjs/mvc/.gitignore.hbs +42 -0
  47. package/templates/nestjs/mvc/Dockerfile.hbs +6 -3
  48. package/templates/nestjs/mvc/docker-compose.yml.hbs +1 -11
  49. package/templates/nestjs/mvc/package.json.hbs +49 -39
  50. package/templates/nestjs/mvc/src/auth/auth.controller.ts.hbs +11 -1
  51. package/templates/nestjs/mvc/src/auth/auth.service.ts.hbs +3 -1
  52. package/templates/nestjs/mvc/src/controllers/health.controller.ts.hbs +6 -6
  53. package/templates/nestjs/mvc/src/main.ts.hbs +1 -4
  54. package/templates/nestjs/mvc/src/models/create-item.dto.ts.hbs +5 -2
  55. package/templates/nestjs/mvc/src/prisma/prisma.service.ts.hbs +1 -0
  56. package/templates/nextjs/mvc/.gitignore.hbs +42 -0
  57. package/templates/nextjs/mvc/Dockerfile.hbs +23 -8
  58. package/templates/nextjs/mvc/docker-compose.yml.hbs +1 -1
  59. package/templates/nextjs/mvc/package.json.hbs +46 -36
  60. package/templates/nodejs-express/clean/.gitignore.hbs +42 -0
  61. package/templates/nodejs-express/clean/Dockerfile.hbs +6 -1
  62. package/templates/nodejs-express/clean/docker-compose.yml.hbs +2 -2
  63. package/templates/nodejs-express/clean/package.json.hbs +12 -2
  64. package/templates/nodejs-express/clean/src/config.ts.hbs +11 -0
  65. package/templates/nodejs-express/clean/src/domain/entities/User.ts.hbs +46 -8
  66. package/templates/nodejs-express/hexagonal/.gitignore.hbs +42 -0
  67. package/templates/nodejs-express/hexagonal/Dockerfile.hbs +1 -1
  68. package/templates/nodejs-express/hexagonal/docker-compose.yml.hbs +2 -2
  69. package/templates/nodejs-express/hexagonal/package.json.hbs +12 -2
  70. package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +21 -38
  71. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts.hbs +2 -0
  72. package/templates/nodejs-express/hexagonal/src/config.ts.hbs +9 -0
  73. package/templates/nodejs-express/hexagonal/src/core/AuthService.ts.hbs +5 -5
  74. package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +7 -22
  75. package/templates/nodejs-express/hexagonal/src/core/domain/entities/User.ts.hbs +24 -4
  76. package/templates/nodejs-express/mvc/.gitignore.hbs +42 -0
  77. package/templates/nodejs-express/mvc/package.json.hbs +12 -2
  78. package/templates/python-fastapi/clean/.gitignore.hbs +76 -0
  79. package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +3 -3
  80. package/templates/python-fastapi/clean/app/config.py.hbs +6 -7
  81. package/templates/python-fastapi/clean/app/domain/usecases/login_user.py.hbs +15 -0
  82. package/templates/python-fastapi/clean/app/infrastructure/http/auth_controller.py.hbs +40 -6
  83. package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +5 -4
  84. package/templates/python-fastapi/clean/app/infrastructure/security/jwt.py.hbs +23 -0
  85. package/templates/python-fastapi/clean/app/main.py.hbs +3 -0
  86. package/templates/python-fastapi/clean/docker-compose.yml.hbs +5 -12
  87. package/templates/python-fastapi/clean/requirements.txt.hbs +3 -0
  88. package/templates/python-fastapi/hexagonal/.gitignore.hbs +76 -0
  89. package/templates/python-fastapi/hexagonal/app/adapters/inbound/http_adapter.py.hbs +6 -9
  90. package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +4 -3
  91. package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +30 -19
  92. package/templates/python-fastapi/hexagonal/app/config.py.hbs +14 -4
  93. package/templates/python-fastapi/hexagonal/app/core/domain/user.py.hbs +3 -1
  94. package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +28 -18
  95. package/templates/python-fastapi/hexagonal/app/core/ports/__init__.py.hbs +3 -0
  96. package/templates/python-fastapi/hexagonal/app/core/ports/user_repository.py.hbs +15 -0
  97. package/templates/python-fastapi/hexagonal/app/infrastructure/database/session.py.hbs +7 -0
  98. package/templates/python-fastapi/hexagonal/app/infrastructure/database/user_repository.py.hbs +53 -0
  99. package/templates/python-fastapi/hexagonal/app/infrastructure/security/__init__.py.hbs +0 -0
  100. package/templates/python-fastapi/hexagonal/app/infrastructure/security/adapters.py.hbs +23 -0
  101. package/templates/python-fastapi/hexagonal/app/infrastructure/security/jwt.py.hbs +23 -0
  102. package/templates/python-fastapi/hexagonal/docker-compose.yml.hbs +5 -12
  103. package/templates/python-fastapi/hexagonal/requirements.txt.hbs +4 -0
  104. package/templates/python-fastapi/mvc/.gitignore.hbs +76 -0
  105. package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +3 -17
  106. package/templates/python-fastapi/mvc/app/middleware/security.py.hbs +24 -3
  107. package/templates/python-fastapi/mvc/app/schemas/item.py.hbs +3 -1
  108. package/templates/python-fastapi/mvc/docker-compose.yml.hbs +5 -12
  109. package/templates/python-fastapi/mvc/requirements.txt.hbs +3 -1
  110. package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts +0 -5
@@ -1,57 +1,40 @@
1
- import { Request, Response, NextFunction } from 'express';
1
+ import { Request, Response } from 'express';
2
2
  import { PaymentService } from '../../../core/PaymentService';
3
3
 
4
4
  export class PaymentController {
5
5
  constructor(private readonly paymentService: PaymentService) {}
6
6
 
7
- /**
8
- * POST /api/payments/checkout
9
- * Requires authentication.
10
- */
11
- createCheckout = async (req: Request, res: Response, next: NextFunction) => {
12
- try {
13
- const userId = (req as any).userId as string;
14
- const { priceId } = req.body;
15
-
16
- if (!priceId) {
17
- return res.status(400).json({ error: 'priceId is required' });
18
- }
7
+ async createCheckoutSession(req: Request, res: Response) {
8
+ const { priceId } = req.body;
9
+ const userId = (req as any).user?.id as string;
19
10
 
11
+ try {
20
12
  const session = await this.paymentService.createCheckoutSession(userId, priceId);
21
13
  res.json({ url: session.url });
22
- } catch (err) {
23
- next(err);
14
+ } catch (error: any) {
15
+ res.status(400).json({ error: error.message });
24
16
  }
25
- };
17
+ }
18
+
19
+ async createPortalSession(req: Request, res: Response) {
20
+ const userId = (req as any).user?.id as string;
26
21
 
27
- /**
28
- * POST /api/payments/portal
29
- * Requires authentication.
30
- */
31
- createPortal = async (req: Request, res: Response, next: NextFunction) => {
32
22
  try {
33
- const userId = (req as any).userId as string;
34
23
  const session = await this.paymentService.createPortalSession(userId);
35
24
  res.json({ url: session.url });
36
- } catch (err) {
37
- next(err);
25
+ } catch (error: any) {
26
+ res.status(400).json({ error: error.message });
38
27
  }
39
- };
28
+ }
29
+
30
+ async handleWebhook(req: Request, res: Response) {
31
+ const sig = req.headers['stripe-signature'] as string;
40
32
 
41
- /**
42
- * POST /api/payments/webhook
43
- * No authentication – Stripe sends the raw body here.
44
- */
45
- handleWebhook = async (req: Request, res: Response, next: NextFunction) => {
46
33
  try {
47
- const signature = req.headers['stripe-signature'] as string;
48
- if (!signature) {
49
- return res.status(400).json({ error: 'Missing stripe-signature header' });
50
- }
51
- const result = await this.paymentService.handleWebhook(req.body, signature);
34
+ const result = await this.paymentService.handleWebhook(req.body, sig);
52
35
  res.json(result);
53
- } catch (err) {
54
- next(err);
36
+ } catch (error: any) {
37
+ res.status(400).send(`Webhook Error: ${error.message}`);
55
38
  }
56
- };
39
+ }
57
40
  }
@@ -0,0 +1,2 @@
1
+ import { PrismaClient } from '@prisma/client';
2
+ export const prisma = new PrismaClient();
@@ -0,0 +1,9 @@
1
+ import dotenv from 'dotenv';
2
+ dotenv.config();
3
+
4
+ export const config = {
5
+ port: process.env.PORT || 3000,
6
+ env: process.env.NODE_ENV || 'development',
7
+ jwtSecret: process.env.JWT_SECRET || 'secret',
8
+ frontendUrl: process.env.FRONTEND_URL || 'http://localhost:3001',
9
+ };
@@ -1,7 +1,7 @@
1
- import { User } from '../domain/entities/User';
2
- import { IAuthPort } from '../ports/inbound/IAuthPort';
3
- import { IUserRepositoryPort } from '../ports/outbound/IUserRepositoryPort';
4
- import { IPasswordHasherPort, ITokenGeneratorPort } from '../ports/outbound/ISecurityPorts';
1
+ import { User } from './domain/entities/User';
2
+ import { IAuthPort } from './ports/inbound/IAuthPort';
3
+ import { IUserRepositoryPort } from './ports/outbound/IUserRepositoryPort';
4
+ import { IPasswordHasherPort, ITokenGeneratorPort } from './ports/outbound/ISecurityPorts';
5
5
 
6
6
  /**
7
7
  * Auth Service - Application Core
@@ -48,4 +48,4 @@ export class AuthService implements IAuthPort {
48
48
  if (!decoded) return null;
49
49
  return this.userRepository.findById(decoded.id);
50
50
  }
51
- }
51
+ }
@@ -1,9 +1,9 @@
1
- import { UserRepository } from './ports/UserRepository';
1
+ import { IUserRepositoryPort } from './ports/outbound/IUserRepositoryPort';
2
2
  import { StripeAdapter } from '../adapters/outbound/StripeAdapter';
3
3
 
4
4
  export class PaymentService {
5
5
  constructor(
6
- private readonly userRepository: UserRepository,
6
+ private readonly userRepository: IUserRepositoryPort,
7
7
  private readonly stripeAdapter: StripeAdapter,
8
8
  ) {}
9
9
 
@@ -17,11 +17,11 @@ export class PaymentService {
17
17
  const customer = await this.stripeAdapter.createCustomer(
18
18
  user.email,
19
19
  user.name,
20
- user.id,
20
+ user.id!,
21
21
  );
22
22
  customerId = customer.id;
23
- user.stripeCustomerId = customerId;
24
- await this.userRepository.save(user);
23
+ const updatedUser = user.withStripeCustomerId(customerId);
24
+ await this.userRepository.save(updatedUser);
25
25
  }
26
26
 
27
27
  return this.stripeAdapter.createCheckoutSession(
@@ -58,28 +58,13 @@ export class PaymentService {
58
58
  if (userId && session.customer) {
59
59
  const user = await this.userRepository.findById(userId);
60
60
  if (user) {
61
- user.stripeCustomerId = session.customer as string;
62
- await this.userRepository.save(user);
61
+ const updatedUser = user.withStripeCustomerId(session.customer as string);
62
+ await this.userRepository.save(updatedUser);
63
63
  }
64
64
  }
65
65
  console.log('Checkout completed for user:', userId);
66
66
  break;
67
67
  }
68
- case 'customer.subscription.updated': {
69
- const sub = event.data.object as any;
70
- console.log('Subscription updated:', sub.id, '| Status:', sub.status);
71
- break;
72
- }
73
- case 'customer.subscription.deleted': {
74
- const sub = event.data.object as any;
75
- console.log('Subscription deleted:', sub.id);
76
- break;
77
- }
78
- case 'invoice.payment_failed': {
79
- const invoice = event.data.object as any;
80
- console.log('Payment failed for invoice:', invoice.id);
81
- break;
82
- }
83
68
  default:
84
69
  console.log('Unhandled Stripe event:', event.type);
85
70
  }
@@ -22,7 +22,27 @@ export class User {
22
22
  }
23
23
 
24
24
  private validatePassword(password: string): void {
25
- if (!password || password.length < 8) { throw new Error('Password must be at least 8 characters'); } } withId(id:
26
- string): User { return new User(id, this.email, this.name, this.password, this.stripeCustomerId, this.createdAt); }
27
- withStripeCustomerId(customerId: string): User { return new User(this.id, this.email, this.name, this.password,
28
- customerId, this.createdAt); } }
25
+ if (!password || password.length < 8) {
26
+ throw new Error('Password must be at least 8 characters');
27
+ }
28
+ }
29
+
30
+ static restore(data: any): User {
31
+ return new User(
32
+ data.id,
33
+ data.email,
34
+ data.name,
35
+ data.password,
36
+ data.stripeCustomerId,
37
+ data.createdAt
38
+ );
39
+ }
40
+
41
+ withId(id: string): User {
42
+ return new User(id, this.email, this.name, this.password, this.stripeCustomerId, this.createdAt);
43
+ }
44
+
45
+ withStripeCustomerId(customerId: string): User {
46
+ return new User(this.id, this.email, this.name, this.password, customerId, this.createdAt);
47
+ }
48
+ }
@@ -0,0 +1,42 @@
1
+ # Dependencies
2
+ node_modules/
3
+ .pnp
4
+ .pnp.js
5
+
6
+ # Build outputs
7
+ dist/
8
+ build/
9
+
10
+ # Environment variables
11
+ .env
12
+ .env.local
13
+ .env.development.local
14
+ .env.test.local
15
+ .env.production.local
16
+ !.env.example
17
+
18
+ # Logs
19
+ logs/
20
+ *.log
21
+ npm-debug.log*
22
+ yarn-debug.log*
23
+ yarn-error.log*
24
+ pnpm-debug.log*
25
+ lerna-debug.log*
26
+
27
+ # Coverage
28
+ coverage/
29
+ .nyc_output
30
+
31
+ # TypeScript
32
+ *.tsbuildinfo
33
+
34
+ # OS
35
+ .DS_Store
36
+ Thumbs.db
37
+
38
+ # Editor
39
+ .vscode/
40
+ .idea/
41
+ *.swp
42
+ *.swo
@@ -44,8 +44,8 @@
44
44
  "@types/bcryptjs": "^2.4.6",
45
45
  "typescript": "^5.3.3",
46
46
  "tsx": "^4.7.1",
47
- "rimraf": "^5.0.5",
48
- "eslint": "^9.0.0",
47
+ "rimraf": "^6.1.2",
48
+ "eslint": "^9.16.0",
49
49
  "@typescript-eslint/eslint-plugin": "^8.0.0",
50
50
  "@typescript-eslint/parser": "^8.0.0",
51
51
  "prettier": "^3.2.5",
@@ -55,5 +55,15 @@
55
55
  },
56
56
  "engines": {
57
57
  "node": ">=18.0.0"
58
+ },
59
+ "overrides": {
60
+ "rimraf": "^6.1.2",
61
+ "glob": "^11.0.0",
62
+ "inflight": "^1.0.6",
63
+ "@humanwhocodes/config-array": "^0.13.0",
64
+ "@humanwhocodes/object-schema": "^2.0.3",
65
+ "eslint": "^9.16.0",
66
+ "cookie": "^0.7.2",
67
+ "minimatch": "^9.0.8"
58
68
  }
59
69
  }
@@ -0,0 +1,76 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.pyo
7
+
8
+ # Distribution / packaging
9
+ build/
10
+ develop-eggs/
11
+ dist/
12
+ downloads/
13
+ eggs/
14
+ .eggs/
15
+ lib/
16
+ lib64/
17
+ parts/
18
+ sdist/
19
+ var/
20
+ wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ .venv/
28
+ venv/
29
+ env/
30
+ ENV/
31
+ env.bak/
32
+ venv.bak/
33
+ .python-version
34
+
35
+ # Environment variables
36
+ .env
37
+ .env.local
38
+ .env.*.local
39
+ !.env.example
40
+
41
+ # Pytest
42
+ .pytest_cache/
43
+ pytest-cache/
44
+ .cache/
45
+
46
+ # Coverage
47
+ htmlcov/
48
+ .tox/
49
+ .coverage
50
+ .coverage.*
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+
55
+ # MyPy / Pyright
56
+ .mypy_cache/
57
+ .dmypy.json
58
+ dmypy.json
59
+ .pyright/
60
+ pyrightconfig.json
61
+
62
+ # Alembic — keep migrations, ignore autogenerated caches
63
+ # alembic/versions/ is intentionally tracked
64
+
65
+ # Jupyter
66
+ .ipynb_checkpoints
67
+
68
+ # OS
69
+ .DS_Store
70
+ Thumbs.db
71
+
72
+ # Editor
73
+ .vscode/
74
+ .idea/
75
+ *.swp
76
+ *.swo
@@ -1,10 +1,10 @@
1
1
  import os
2
- from app.domain.repositories.user_repository import UserRepository
2
+ from app.domain.repositories.user_repository import IUserRepository
3
3
  from app.infrastructure.stripe_provider import StripeProvider
4
4
 
5
5
 
6
6
  class PaymentService:
7
- def __init__(self, user_repository: UserRepository, stripe_provider: StripeProvider):
7
+ def __init__(self, user_repository: IUserRepository, stripe_provider: StripeProvider):
8
8
  self.user_repository = user_repository
9
9
  self.stripe_provider = stripe_provider
10
10
 
@@ -30,7 +30,7 @@ class PaymentService:
30
30
  customer_id=customer_id,
31
31
  price_id=price_id,
32
32
  user_id=str(user_id),
33
- success_url=f"{os.getenv('FRONTEND_URL')}/success?session_id={{CHECKOUT_SESSION_ID}}",
33
+ success_url=f"{os.getenv('FRONTEND_URL')}/success?session_id={'{CHECKOUT_SESSION_ID}'}",
34
34
  cancel_url=f"{os.getenv('FRONTEND_URL')}/cancel",
35
35
  )
36
36
  return session.url
@@ -4,20 +4,19 @@ from functools import lru_cache
4
4
  class Settings(BaseSettings):
5
5
  PROJECT_NAME: str = "{{projectName}}"
6
6
  API_V1_STR: str = "/api/v1"
7
-
8
- # Database
9
- # Use asyncpg for async SQLAlchemy
10
- DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/{{projectName}}_db"
11
-
7
+
8
+ # Database — use asyncpg for async SQLAlchemy
9
+ DATABASE_URL: str = "postgresql+asyncpg://postgres:postgres@localhost:5432/{{projectName}}"
10
+
12
11
  # Security
13
12
  SECRET_KEY: str = "change_this_to_a_secure_random_key"
14
13
  ALGORITHM: str = "HS256"
15
14
  ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
16
-
15
+
17
16
  # Stripe
18
17
  STRIPE_API_KEY: str | None = None
19
18
 
20
- model_config = SettingsConfigDict(env_file=".env", case_sensitive=True)
19
+ model_config = SettingsConfigDict(env_file=".env", case_sensitive=True, extra="ignore")
21
20
 
22
21
  @lru_cache
23
22
  def get_settings():
@@ -0,0 +1,15 @@
1
+ from app.domain.repositories.user_repository import IUserRepository
2
+
3
+
4
+ class LoginUserUseCase:
5
+ def __init__(self, repo: IUserRepository, hasher, token_gen):
6
+ self.repo = repo
7
+ self.hasher = hasher
8
+ self.token_gen = token_gen
9
+
10
+ async def execute(self, email: str, password: str) -> dict:
11
+ user = await self.repo.find_by_email(email)
12
+ if not user or not self.hasher.verify(password, user.password):
13
+ raise ValueError("Invalid email or password")
14
+ token = self.token_gen.generate(user.id, user.email)
15
+ return {"user": user, "token": token}
@@ -2,25 +2,48 @@ from fastapi import APIRouter, HTTPException, Depends
2
2
  from pydantic import BaseModel, EmailStr
3
3
  from sqlalchemy.ext.asyncio import AsyncSession
4
4
  from app.domain.usecases.register_user import RegisterUserUseCase
5
+ from app.domain.usecases.login_user import LoginUserUseCase
5
6
  from app.infrastructure.database.postgres_repository import PostgresUserRepository
6
7
  from app.infrastructure.security.adapters import BcryptHasher, JwtTokenGenerator
7
8
  from app.infrastructure.database.session import get_db
8
9
 
9
10
  router = APIRouter()
10
11
 
11
- # Dependency Injection Factory
12
+
13
+ class RegisterRequest(BaseModel):
14
+ email: EmailStr
15
+ name: str
16
+ password: str
17
+
18
+ class LoginRequest(BaseModel):
19
+ email: EmailStr
20
+ password: str
21
+
22
+ class UserResponse(BaseModel):
23
+ id: str
24
+ email: str
25
+ name: str
26
+
27
+ class AuthResponse(BaseModel):
28
+ user: UserResponse
29
+ token: str
30
+
31
+
32
+ # Dependency Injection Factories
12
33
  def get_register_usecase(db: AsyncSession = Depends(get_db)) -> RegisterUserUseCase:
13
34
  repo = PostgresUserRepository(db)
14
35
  hasher = BcryptHasher()
15
36
  token_gen = JwtTokenGenerator()
16
37
  return RegisterUserUseCase(repo, hasher, token_gen)
17
38
 
18
- class RegisterRequest(BaseModel):
19
- email: EmailStr
20
- name: str
21
- password: str
39
+ def get_login_usecase(db: AsyncSession = Depends(get_db)) -> LoginUserUseCase:
40
+ repo = PostgresUserRepository(db)
41
+ hasher = BcryptHasher()
42
+ token_gen = JwtTokenGenerator()
43
+ return LoginUserUseCase(repo, hasher, token_gen)
22
44
 
23
- @router.post("/register")
45
+
46
+ @router.post("/register", response_model=AuthResponse)
24
47
  async def register(
25
48
  req: RegisterRequest,
26
49
  usecase: RegisterUserUseCase = Depends(get_register_usecase)
@@ -30,3 +53,14 @@ async def register(
30
53
  return result
31
54
  except ValueError as e:
32
55
  raise HTTPException(status_code=400, detail=str(e))
56
+
57
+ @router.post("/login", response_model=AuthResponse)
58
+ async def login(
59
+ req: LoginRequest,
60
+ usecase: LoginUserUseCase = Depends(get_login_usecase)
61
+ ):
62
+ try:
63
+ result = await usecase.execute(req.email, req.password)
64
+ return result
65
+ except ValueError as e:
66
+ raise HTTPException(status_code=401, detail=str(e))
@@ -1,16 +1,17 @@
1
1
  from fastapi import APIRouter, HTTPException, Header, Request, Depends
2
2
  from pydantic import BaseModel
3
+ from sqlalchemy.ext.asyncio import AsyncSession
3
4
  from app.application.services.payment_service import PaymentService
4
- from app.infrastructure.database.session import AsyncSessionLocal
5
- from app.infrastructure.database.user_repository import SQLAlchemyUserRepository
5
+ from app.infrastructure.database.session import get_db
6
+ from app.infrastructure.database.postgres_repository import PostgresUserRepository
6
7
  from app.infrastructure.stripe_provider import StripeProvider
7
8
  from app.infrastructure.security.jwt import get_current_user_id
8
9
 
9
10
  router = APIRouter()
10
11
 
11
12
 
12
- def get_payment_service() -> PaymentService:
13
- repo = SQLAlchemyUserRepository(AsyncSessionLocal())
13
+ def get_payment_service(db: AsyncSession = Depends(get_db)) -> PaymentService:
14
+ repo = PostgresUserRepository(db)
14
15
  provider = StripeProvider()
15
16
  return PaymentService(user_repository=repo, stripe_provider=provider)
16
17
 
@@ -0,0 +1,23 @@
1
+ from fastapi import HTTPException, Security
2
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
3
+ from jose import jwt, JWTError
4
+ import os
5
+
6
+ security = HTTPBearer()
7
+
8
+ SECRET_KEY = os.getenv("JWT_SECRET", "secret")
9
+ ALGORITHM = "HS256"
10
+
11
+
12
+ def get_current_user_id(
13
+ credentials: HTTPAuthorizationCredentials = Security(security),
14
+ ) -> str:
15
+ try:
16
+ token = credentials.credentials
17
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
18
+ user_id: str = payload.get("sub")
19
+ if user_id is None:
20
+ raise HTTPException(status_code=401, detail="Invalid authentication credentials")
21
+ return user_id
22
+ except JWTError:
23
+ raise HTTPException(status_code=401, detail="Invalid authentication credentials")
@@ -1,3 +1,6 @@
1
+ from dotenv import load_dotenv
2
+ load_dotenv()
3
+
1
4
  from contextlib import asynccontextmanager
2
5
  from fastapi import FastAPI
3
6
  from app.infrastructure.http import auth_controller, payment_controller
@@ -1,16 +1,4 @@
1
- version: '3.8'
2
-
3
1
  services:
4
- app:
5
- build: .
6
- container_name: {{kebabCase projectName}}-app
7
- ports:
8
- - "8000:8000"
9
- environment:
10
- - DATABASE_URL=postgresql://postgres:postgres@db:5432/{{snakeCase projectName}}
11
- depends_on:
12
- - db
13
-
14
2
  db:
15
3
  image: postgres:15-alpine
16
4
  container_name: {{kebabCase projectName}}-db
@@ -23,6 +11,11 @@ services:
23
11
  volumes:
24
12
  - postgres_data:/var/lib/postgresql/data
25
13
  restart: unless-stopped
14
+ healthcheck:
15
+ test: ["CMD-SHELL", "pg_isready -U postgres"]
16
+ interval: 10s
17
+ timeout: 5s
18
+ retries: 5
26
19
 
27
20
  volumes:
28
21
  postgres_data:
@@ -1,10 +1,13 @@
1
1
  fastapi>=0.109.0
2
2
  uvicorn[standard]>=0.27.0
3
3
  pydantic>=2.5.0
4
+ pydantic[email]>=2.5.0
4
5
  pydantic-settings>=2.1.0
5
6
  python-dotenv>=1.0.0
6
7
  python-jose[cryptography]>=3.3.0
7
8
  passlib[bcrypt]>=1.7.4
9
+ bcrypt>=3.0.0,<4.0.0
10
+ greenlet>=3.0.0
8
11
  stripe>=8.0.0
9
12
  sqlalchemy>=2.0.0
10
13
  alembic>=1.13.0
@@ -0,0 +1,76 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ *.pyo
7
+
8
+ # Distribution / packaging
9
+ build/
10
+ develop-eggs/
11
+ dist/
12
+ downloads/
13
+ eggs/
14
+ .eggs/
15
+ lib/
16
+ lib64/
17
+ parts/
18
+ sdist/
19
+ var/
20
+ wheels/
21
+ *.egg-info/
22
+ .installed.cfg
23
+ *.egg
24
+ MANIFEST
25
+
26
+ # Virtual environments
27
+ .venv/
28
+ venv/
29
+ env/
30
+ ENV/
31
+ env.bak/
32
+ venv.bak/
33
+ .python-version
34
+
35
+ # Environment variables
36
+ .env
37
+ .env.local
38
+ .env.*.local
39
+ !.env.example
40
+
41
+ # Pytest
42
+ .pytest_cache/
43
+ pytest-cache/
44
+ .cache/
45
+
46
+ # Coverage
47
+ htmlcov/
48
+ .tox/
49
+ .coverage
50
+ .coverage.*
51
+ coverage.xml
52
+ *.cover
53
+ *.py,cover
54
+
55
+ # MyPy / Pyright
56
+ .mypy_cache/
57
+ .dmypy.json
58
+ dmypy.json
59
+ .pyright/
60
+ pyrightconfig.json
61
+
62
+ # Alembic — keep migrations, ignore autogenerated caches
63
+ # alembic/versions/ is intentionally tracked
64
+
65
+ # Jupyter
66
+ .ipynb_checkpoints
67
+
68
+ # OS
69
+ .DS_Store
70
+ Thumbs.db
71
+
72
+ # Editor
73
+ .vscode/
74
+ .idea/
75
+ *.swp
76
+ *.swo