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.
- package/README.md +1 -1
- package/package.json +2 -2
- package/templates/java-spring/clean/.gitignore.hbs +72 -0
- package/templates/java-spring/clean/docker-compose.yml.hbs +6 -3
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/application/usecase/PaymentUseCase.java.hbs +21 -17
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/entity/UserEntity.java.hbs +52 -0
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/persistence/repository/JpaUserRepository.java.hbs +12 -0
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/JwtAuthenticationFilter.java.hbs +64 -0
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/security/SecurityConfig.java.hbs +36 -0
- package/templates/java-spring/clean/src/main/java/{{packagePath}}/infrastructure/stripe/StripeGateway.java.hbs +63 -0
- package/templates/java-spring/clean/src/main/resources/application.properties.hbs +6 -7
- package/templates/java-spring/hexagonal/.gitignore.hbs +72 -0
- package/templates/java-spring/hexagonal/docker-compose.yml.hbs +6 -3
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/JwtFilter.java.hbs +71 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/adapters/outbound/security/SecurityConfig.java.hbs +35 -0
- package/templates/java-spring/hexagonal/src/main/java/{{packagePath}}/core/service/PaymentService.java.hbs +3 -3
- package/templates/java-spring/hexagonal/src/main/resources/application.properties.hbs +4 -4
- package/templates/java-spring/mvc/.gitignore.hbs +72 -0
- package/templates/java-spring/mvc/docker-compose.yml.hbs +6 -3
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/config/SecurityConfig.java.hbs +13 -12
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/AuthController.java.hbs +9 -8
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/PaymentsController.java.hbs +5 -6
- package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs +3 -3
- package/templates/java-spring/mvc/src/main/resources/application.yml.hbs +29 -26
- package/templates/nestjs/clean/.gitignore.hbs +42 -0
- package/templates/nestjs/clean/Dockerfile.hbs +6 -3
- package/templates/nestjs/clean/docker-compose.yml.hbs +1 -11
- package/templates/nestjs/clean/package.json.hbs +51 -41
- package/templates/nestjs/clean/src/app.module.ts.hbs +2 -1
- package/templates/nestjs/clean/src/application/payment.service.ts.hbs +72 -72
- package/templates/nestjs/clean/src/domain/entities/user.entity.ts.hbs +2 -2
- package/templates/nestjs/clean/src/domain/repositories/user.repository.ts.hbs +2 -2
- package/templates/nestjs/clean/src/infrastructure/database/repositories/prisma.user.repository.ts.hbs +18 -18
- package/templates/nestjs/clean/src/infrastructure/http/health.controller.ts.hbs +9 -0
- package/templates/nestjs/clean/src/main.ts.hbs +1 -4
- package/templates/nestjs/clean/src/payment.module.ts.hbs +12 -12
- package/templates/nestjs/hexagonal/.gitignore.hbs +42 -0
- package/templates/nestjs/hexagonal/Dockerfile.hbs +6 -3
- package/templates/nestjs/hexagonal/docker-compose.yml.hbs +1 -11
- package/templates/nestjs/hexagonal/package.json.hbs +51 -41
- package/templates/nestjs/hexagonal/src/adapters/inbound/health.controller.ts.hbs +9 -0
- package/templates/nestjs/hexagonal/src/app.module.ts.hbs +2 -1
- package/templates/nestjs/hexagonal/src/core/domain/user.entity.ts.hbs +6 -6
- package/templates/nestjs/hexagonal/src/core/ports/ports.ts.hbs +4 -4
- package/templates/nestjs/hexagonal/src/main.ts.hbs +1 -4
- package/templates/nestjs/mvc/.gitignore.hbs +42 -0
- package/templates/nestjs/mvc/Dockerfile.hbs +6 -3
- package/templates/nestjs/mvc/docker-compose.yml.hbs +1 -11
- package/templates/nestjs/mvc/package.json.hbs +49 -39
- package/templates/nestjs/mvc/src/auth/auth.controller.ts.hbs +11 -1
- package/templates/nestjs/mvc/src/auth/auth.service.ts.hbs +3 -1
- package/templates/nestjs/mvc/src/controllers/health.controller.ts.hbs +6 -6
- package/templates/nestjs/mvc/src/main.ts.hbs +1 -4
- package/templates/nestjs/mvc/src/models/create-item.dto.ts.hbs +5 -2
- package/templates/nestjs/mvc/src/prisma/prisma.service.ts.hbs +1 -0
- package/templates/nextjs/mvc/.gitignore.hbs +42 -0
- package/templates/nextjs/mvc/Dockerfile.hbs +23 -8
- package/templates/nextjs/mvc/docker-compose.yml.hbs +1 -1
- package/templates/nextjs/mvc/package.json.hbs +46 -36
- package/templates/nodejs-express/clean/.gitignore.hbs +42 -0
- package/templates/nodejs-express/clean/Dockerfile.hbs +6 -1
- package/templates/nodejs-express/clean/docker-compose.yml.hbs +2 -2
- package/templates/nodejs-express/clean/package.json.hbs +12 -2
- package/templates/nodejs-express/clean/src/config.ts.hbs +11 -0
- package/templates/nodejs-express/clean/src/domain/entities/User.ts.hbs +46 -8
- package/templates/nodejs-express/hexagonal/.gitignore.hbs +42 -0
- package/templates/nodejs-express/hexagonal/Dockerfile.hbs +1 -1
- package/templates/nodejs-express/hexagonal/docker-compose.yml.hbs +2 -2
- package/templates/nodejs-express/hexagonal/package.json.hbs +12 -2
- package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs +21 -38
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts.hbs +2 -0
- package/templates/nodejs-express/hexagonal/src/config.ts.hbs +9 -0
- package/templates/nodejs-express/hexagonal/src/core/AuthService.ts.hbs +5 -5
- package/templates/nodejs-express/hexagonal/src/core/PaymentService.ts.hbs +7 -22
- package/templates/nodejs-express/hexagonal/src/core/domain/entities/User.ts.hbs +24 -4
- package/templates/nodejs-express/mvc/.gitignore.hbs +42 -0
- package/templates/nodejs-express/mvc/package.json.hbs +12 -2
- package/templates/python-fastapi/clean/.gitignore.hbs +76 -0
- package/templates/python-fastapi/clean/app/application/services/payment_service.py.hbs +3 -3
- package/templates/python-fastapi/clean/app/config.py.hbs +6 -7
- package/templates/python-fastapi/clean/app/domain/usecases/login_user.py.hbs +15 -0
- package/templates/python-fastapi/clean/app/infrastructure/http/auth_controller.py.hbs +40 -6
- package/templates/python-fastapi/clean/app/infrastructure/http/payment_controller.py.hbs +5 -4
- package/templates/python-fastapi/clean/app/infrastructure/security/jwt.py.hbs +23 -0
- package/templates/python-fastapi/clean/app/main.py.hbs +3 -0
- package/templates/python-fastapi/clean/docker-compose.yml.hbs +5 -12
- package/templates/python-fastapi/clean/requirements.txt.hbs +3 -0
- package/templates/python-fastapi/hexagonal/.gitignore.hbs +76 -0
- package/templates/python-fastapi/hexagonal/app/adapters/inbound/http_adapter.py.hbs +6 -9
- package/templates/python-fastapi/hexagonal/app/adapters/inbound/payment_http_adapter.py.hbs +4 -3
- package/templates/python-fastapi/hexagonal/app/adapters/outbound/stripe_adapter.py.hbs +30 -19
- package/templates/python-fastapi/hexagonal/app/config.py.hbs +14 -4
- package/templates/python-fastapi/hexagonal/app/core/domain/user.py.hbs +3 -1
- package/templates/python-fastapi/hexagonal/app/core/payment_service.py.hbs +28 -18
- package/templates/python-fastapi/hexagonal/app/core/ports/__init__.py.hbs +3 -0
- package/templates/python-fastapi/hexagonal/app/core/ports/user_repository.py.hbs +15 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/database/session.py.hbs +7 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/database/user_repository.py.hbs +53 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/security/__init__.py.hbs +0 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/security/adapters.py.hbs +23 -0
- package/templates/python-fastapi/hexagonal/app/infrastructure/security/jwt.py.hbs +23 -0
- package/templates/python-fastapi/hexagonal/docker-compose.yml.hbs +5 -12
- package/templates/python-fastapi/hexagonal/requirements.txt.hbs +4 -0
- package/templates/python-fastapi/mvc/.gitignore.hbs +76 -0
- package/templates/python-fastapi/mvc/app/controllers/payments.py.hbs +3 -17
- package/templates/python-fastapi/mvc/app/middleware/security.py.hbs +24 -3
- package/templates/python-fastapi/mvc/app/schemas/item.py.hbs +3 -1
- package/templates/python-fastapi/mvc/docker-compose.yml.hbs +5 -12
- package/templates/python-fastapi/mvc/requirements.txt.hbs +3 -1
- package/templates/nodejs-express/hexagonal/src/adapters/outbound/persistence/prisma.ts +0 -5
package/templates/nodejs-express/hexagonal/src/adapters/inbound/http/PaymentController.ts.hbs
CHANGED
|
@@ -1,57 +1,40 @@
|
|
|
1
|
-
import { Request, Response
|
|
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
|
-
|
|
9
|
-
|
|
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 (
|
|
23
|
-
|
|
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 (
|
|
37
|
-
|
|
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
|
|
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 (
|
|
54
|
-
|
|
36
|
+
} catch (error: any) {
|
|
37
|
+
res.status(400).send(`Webhook Error: ${error.message}`);
|
|
55
38
|
}
|
|
56
|
-
}
|
|
39
|
+
}
|
|
57
40
|
}
|
|
@@ -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 '
|
|
2
|
-
import { IAuthPort } from '
|
|
3
|
-
import { IUserRepositoryPort } from '
|
|
4
|
-
import { IPasswordHasherPort, ITokenGeneratorPort } from '
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
24
|
-
await this.userRepository.save(
|
|
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
|
-
|
|
62
|
-
await this.userRepository.save(
|
|
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) {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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": "^
|
|
48
|
-
"eslint": "^9.
|
|
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
|
|
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:
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
5
|
-
from app.infrastructure.database.
|
|
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 =
|
|
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,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
|