kybernus 3.0.1 → 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 +1 -1
- 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/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/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/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/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 +69 -69
- 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 +69 -69
- 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 +67 -67
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
spring.application.name={{projectNameKebabCase}}
|
|
2
2
|
|
|
3
3
|
# Database Configuration
|
|
4
|
-
spring.datasource.url=jdbc:postgresql
|
|
4
|
+
spring.datasource.url=jdbc:postgresql://${DB_HOST:localhost}:5432/{{projectNameKebabCase}}
|
|
5
5
|
spring.datasource.username=postgres
|
|
6
6
|
spring.datasource.password=postgres
|
|
7
7
|
spring.datasource.driver-class-name=org.postgresql.Driver
|
|
@@ -13,13 +13,13 @@ spring.jpa.show-sql=true
|
|
|
13
13
|
spring.jpa.properties.hibernate.format_sql=true
|
|
14
14
|
|
|
15
15
|
# JWT
|
|
16
|
-
application.security.jwt.secret-key
|
|
16
|
+
application.security.jwt.secret-key=${JWT_SECRET:{{jwtSecretKey}}}
|
|
17
17
|
application.security.jwt.expiration=86400000
|
|
18
18
|
application.security.jwt.refresh-token.expiration=604800000
|
|
19
19
|
|
|
20
20
|
# Stripe
|
|
21
|
-
stripe.secret-key=${STRIPE_SECRET_KEY}
|
|
22
|
-
stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET}
|
|
21
|
+
stripe.secret-key=${STRIPE_SECRET_KEY:sk_test_dummy}
|
|
22
|
+
stripe.webhook-secret=${STRIPE_WEBHOOK_SECRET:whsec_dummy}
|
|
23
23
|
|
|
24
24
|
# Frontend URL (for Stripe redirect URLs)
|
|
25
25
|
frontend.url=${FRONTEND_URL:http://localhost:3000}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Compiled class files
|
|
2
|
+
*.class
|
|
3
|
+
|
|
4
|
+
# Log files
|
|
5
|
+
*.log
|
|
6
|
+
|
|
7
|
+
# Package files
|
|
8
|
+
*.jar
|
|
9
|
+
*.war
|
|
10
|
+
*.nar
|
|
11
|
+
*.ear
|
|
12
|
+
*.zip
|
|
13
|
+
*.tar.gz
|
|
14
|
+
*.rar
|
|
15
|
+
|
|
16
|
+
# Virtual machine crash logs
|
|
17
|
+
hs_err_pid*
|
|
18
|
+
replay_pid*
|
|
19
|
+
|
|
20
|
+
# Maven
|
|
21
|
+
target/
|
|
22
|
+
.mvn/wrapper/maven-wrapper.jar
|
|
23
|
+
!**/src/main/**/target/
|
|
24
|
+
!**/src/test/**/target/
|
|
25
|
+
|
|
26
|
+
# Gradle
|
|
27
|
+
.gradle
|
|
28
|
+
build/
|
|
29
|
+
!gradle/wrapper/gradle-wrapper.jar
|
|
30
|
+
!**/src/main/**/build/
|
|
31
|
+
!**/src/test/**/build/
|
|
32
|
+
|
|
33
|
+
# Spring Boot
|
|
34
|
+
spring-shell.log
|
|
35
|
+
|
|
36
|
+
# STS / Eclipse
|
|
37
|
+
.apt_generated
|
|
38
|
+
.classpath
|
|
39
|
+
.factorypath
|
|
40
|
+
.project
|
|
41
|
+
.settings
|
|
42
|
+
.springBeans
|
|
43
|
+
.sts4-cache
|
|
44
|
+
|
|
45
|
+
# IntelliJ IDEA
|
|
46
|
+
.idea/
|
|
47
|
+
*.iws
|
|
48
|
+
*.iml
|
|
49
|
+
*.ipr
|
|
50
|
+
|
|
51
|
+
# VS Code
|
|
52
|
+
.vscode/
|
|
53
|
+
|
|
54
|
+
# NetBeans
|
|
55
|
+
/nbproject/private/
|
|
56
|
+
/nbbuild/
|
|
57
|
+
/nbdist/
|
|
58
|
+
/.nb-gradle/
|
|
59
|
+
|
|
60
|
+
# Environment
|
|
61
|
+
.env
|
|
62
|
+
.env.local
|
|
63
|
+
.env.*.local
|
|
64
|
+
!.env.example
|
|
65
|
+
application-local.yml
|
|
66
|
+
application-local.properties
|
|
67
|
+
application-dev.yml
|
|
68
|
+
application-dev.properties
|
|
69
|
+
|
|
70
|
+
# OS
|
|
71
|
+
.DS_Store
|
|
72
|
+
Thumbs.db
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
version: '3.8'
|
|
2
|
-
|
|
3
1
|
services:
|
|
4
|
-
|
|
2
|
+
db:
|
|
5
3
|
image: postgres:15-alpine
|
|
6
4
|
container_name: {{projectNameKebabCase}}-db
|
|
7
5
|
environment:
|
|
@@ -13,6 +11,11 @@ services:
|
|
|
13
11
|
volumes:
|
|
14
12
|
- postgres_data:/var/lib/postgresql/data
|
|
15
13
|
restart: unless-stopped
|
|
14
|
+
healthcheck:
|
|
15
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
16
|
+
interval: 10s
|
|
17
|
+
timeout: 5s
|
|
18
|
+
retries: 5
|
|
16
19
|
|
|
17
20
|
volumes:
|
|
18
21
|
postgres_data:
|
package/templates/java-spring/mvc/src/main/java/{{packagePath}}/config/SecurityConfig.java.hbs
CHANGED
|
@@ -27,18 +27,19 @@ public class SecurityConfig {
|
|
|
27
27
|
.csrf(csrf -> csrf.disable())
|
|
28
28
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
29
29
|
.authorizeHttpRequests(auth -> auth
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
.requestMatchers("/api/auth/**").permitAll()
|
|
31
|
+
.requestMatchers("/api/payments/webhook").permitAll()
|
|
32
|
+
.requestMatchers("/api/health").permitAll()
|
|
33
|
+
.requestMatchers("/actuator/health").permitAll()
|
|
34
|
+
.anyRequest().authenticated()
|
|
35
|
+
)
|
|
36
|
+
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
}
|
|
38
|
+
return http.build();
|
|
39
|
+
}
|
|
39
40
|
|
|
40
|
-
@Bean
|
|
41
|
-
public PasswordEncoder passwordEncoder() {
|
|
42
|
-
|
|
41
|
+
@Bean
|
|
42
|
+
public PasswordEncoder passwordEncoder() {
|
|
43
|
+
return new BCryptPasswordEncoder();
|
|
44
|
+
}
|
|
43
45
|
}
|
|
44
|
-
}
|
package/templates/java-spring/mvc/src/main/java/{{packagePath}}/controller/AuthController.java.hbs
CHANGED
|
@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
|
|
|
10
10
|
import org.springframework.security.core.context.SecurityContextHolder;
|
|
11
11
|
import org.springframework.security.core.Authentication;
|
|
12
12
|
|
|
13
|
+
import java.util.HashMap;
|
|
13
14
|
import java.util.Map;
|
|
14
15
|
|
|
15
16
|
@RestController
|
|
@@ -48,17 +49,17 @@ public class AuthController {
|
|
|
48
49
|
if (authentication == null || !authentication.isAuthenticated()) {
|
|
49
50
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
|
|
50
51
|
}
|
|
51
|
-
|
|
52
|
+
|
|
52
53
|
try {
|
|
53
|
-
String userId = (String) authentication.getPrincipal();
|
|
54
|
+
String userId = (String) authentication.getPrincipal();
|
|
54
55
|
User user = authService.getMe(userId);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
));
|
|
56
|
+
// Use HashMap instead of Map.of() — tolerates null values (e.g. name may be null)
|
|
57
|
+
Map<String, Object> result = new HashMap<>();
|
|
58
|
+
result.put("id", user.getId());
|
|
59
|
+
result.put("email", user.getEmail());
|
|
60
|
+
result.put("name", user.getName());
|
|
61
|
+
return ResponseEntity.ok(result);
|
|
60
62
|
} catch (Exception e) {
|
|
61
|
-
// Keep original behavior if principal isn't id
|
|
62
63
|
return ResponseEntity.ok(Map.of("message", "Authenticated"));
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -6,7 +6,6 @@ import org.slf4j.LoggerFactory;
|
|
|
6
6
|
import org.springframework.http.HttpStatus;
|
|
7
7
|
import org.springframework.http.ResponseEntity;
|
|
8
8
|
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
|
9
|
-
import org.springframework.security.core.userdetails.UserDetails;
|
|
10
9
|
import org.springframework.web.bind.annotation.*;
|
|
11
10
|
|
|
12
11
|
import java.util.Map;
|
|
@@ -29,16 +28,16 @@ public class PaymentsController {
|
|
|
29
28
|
*/
|
|
30
29
|
@PostMapping("/checkout")
|
|
31
30
|
public ResponseEntity<?> createCheckout(
|
|
32
|
-
@AuthenticationPrincipal
|
|
31
|
+
@AuthenticationPrincipal String userId,
|
|
33
32
|
@RequestBody Map<String, String> body) {
|
|
34
33
|
try {
|
|
35
34
|
String priceId = body.get("priceId");
|
|
36
35
|
if (priceId == null || priceId.isEmpty()) {
|
|
37
36
|
return ResponseEntity.badRequest().body(Map.of("error", "priceId is required"));
|
|
38
37
|
}
|
|
39
|
-
//
|
|
38
|
+
// userId is set directly in SecurityContextHolder by JwtAuthenticationFilter
|
|
40
39
|
com.stripe.model.checkout.Session session =
|
|
41
|
-
stripeService.createCheckoutSession(
|
|
40
|
+
stripeService.createCheckoutSession(userId, priceId);
|
|
42
41
|
return ResponseEntity.ok(Map.of("url", session.getUrl()));
|
|
43
42
|
} catch (Exception e) {
|
|
44
43
|
logger.error("Error creating checkout session", e);
|
|
@@ -51,10 +50,10 @@ public class PaymentsController {
|
|
|
51
50
|
* Requires authentication. Opens Stripe Billing Portal.
|
|
52
51
|
*/
|
|
53
52
|
@PostMapping("/portal")
|
|
54
|
-
public ResponseEntity<?> createPortal(@AuthenticationPrincipal
|
|
53
|
+
public ResponseEntity<?> createPortal(@AuthenticationPrincipal String userId) {
|
|
55
54
|
try {
|
|
56
55
|
com.stripe.model.billingportal.Session session =
|
|
57
|
-
stripeService.createPortalSession(
|
|
56
|
+
stripeService.createPortalSession(userId);
|
|
58
57
|
return ResponseEntity.ok(Map.of("url", session.getUrl()));
|
|
59
58
|
} catch (Exception e) {
|
|
60
59
|
logger.error("Error creating portal session", e);
|
package/templates/java-spring/mvc/src/main/java/{{packagePath}}/service/StripeService.java.hbs
CHANGED
|
@@ -45,7 +45,7 @@ public class StripeService {
|
|
|
45
45
|
* Retrieve or create a Stripe customer for the given user, then open a checkout session.
|
|
46
46
|
*/
|
|
47
47
|
public Session createCheckoutSession(String userId, String priceId) throws StripeException {
|
|
48
|
-
User user = userRepository.findById(
|
|
48
|
+
User user = userRepository.findById(userId)
|
|
49
49
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
|
50
50
|
|
|
51
51
|
String customerId = user.getStripeCustomerId();
|
|
@@ -81,7 +81,7 @@ public class StripeService {
|
|
|
81
81
|
* Open the Stripe Billing Portal for the given customer.
|
|
82
82
|
*/
|
|
83
83
|
public com.stripe.model.billingportal.Session createPortalSession(String userId) throws StripeException {
|
|
84
|
-
User user = userRepository.findById(
|
|
84
|
+
User user = userRepository.findById(userId)
|
|
85
85
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
|
86
86
|
|
|
87
87
|
if (user.getStripeCustomerId() == null) {
|
|
@@ -113,7 +113,7 @@ public class StripeService {
|
|
|
113
113
|
Session session = (Session) event.getDataObjectDeserializer().getObject().orElse(null);
|
|
114
114
|
if (session != null && session.getClientReferenceId() != null) {
|
|
115
115
|
String userId = session.getClientReferenceId();
|
|
116
|
-
userRepository.findById(
|
|
116
|
+
userRepository.findById(userId).ifPresent(user -> {
|
|
117
117
|
user.setStripeCustomerId(session.getCustomer());
|
|
118
118
|
userRepository.save(user);
|
|
119
119
|
});
|
|
@@ -1,32 +1,35 @@
|
|
|
1
1
|
spring:
|
|
2
2
|
application:
|
|
3
3
|
name: {{projectNameKebabCase}}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
format_sql: true
|
|
18
|
-
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
4
|
+
datasource:
|
|
5
|
+
url: jdbc:postgresql://localhost:5432/{{projectNameSnakeCase}}
|
|
6
|
+
username: postgres
|
|
7
|
+
password: postgres
|
|
8
|
+
driver-class-name: org.postgresql.Driver
|
|
9
|
+
jpa:
|
|
10
|
+
hibernate:
|
|
11
|
+
ddl-auto: update
|
|
12
|
+
show-sql: true
|
|
13
|
+
properties:
|
|
14
|
+
hibernate:
|
|
15
|
+
format_sql: true
|
|
16
|
+
dialect: org.hibernate.dialect.PostgreSQLDialect
|
|
19
17
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
console:
|
|
23
|
-
enabled: true
|
|
24
|
-
path: /h2-console
|
|
18
|
+
server:
|
|
19
|
+
port: 8080
|
|
25
20
|
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
logging:
|
|
22
|
+
level:
|
|
23
|
+
root: INFO
|
|
24
|
+
{{packageName}}: DEBUG
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
jwt:
|
|
27
|
+
secret: ${JWT_SECRET:{{jwtSecretKey}}}
|
|
28
|
+
expiration: 604800000
|
|
29
|
+
|
|
30
|
+
stripe:
|
|
31
|
+
secret-key: ${STRIPE_SECRET_KEY:sk_test_dummy}
|
|
32
|
+
webhook-secret: ${STRIPE_WEBHOOK_SECRET:whsec_dummy}
|
|
33
|
+
|
|
34
|
+
frontend:
|
|
35
|
+
url: ${FRONTEND_URL:http://localhost:3000}
|
|
@@ -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
|
|
@@ -9,7 +9,7 @@ WORKDIR /app
|
|
|
9
9
|
COPY package*.json ./
|
|
10
10
|
|
|
11
11
|
# Install dependencies
|
|
12
|
-
RUN npm ci
|
|
12
|
+
RUN npm ci
|
|
13
13
|
|
|
14
14
|
# Copy source code
|
|
15
15
|
COPY . .
|
|
@@ -26,6 +26,9 @@ WORKDIR /app
|
|
|
26
26
|
RUN addgroup -g 1001 -S nodejs && \
|
|
27
27
|
adduser -S nodejs -u 1001
|
|
28
28
|
|
|
29
|
+
# Add openssl
|
|
30
|
+
RUN apk add --no-cache openssl
|
|
31
|
+
|
|
29
32
|
# Copy built application
|
|
30
33
|
COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist
|
|
31
34
|
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
|
|
@@ -39,7 +42,7 @@ EXPOSE 3000
|
|
|
39
42
|
|
|
40
43
|
# Health check
|
|
41
44
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
42
|
-
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
|
|
45
|
+
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/api/health || exit 1
|
|
43
46
|
|
|
44
47
|
# Start application
|
|
45
|
-
CMD
|
|
48
|
+
CMD node dist/main.js
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
version: '3.8'
|
|
2
2
|
|
|
3
3
|
services:
|
|
4
|
-
app:
|
|
5
|
-
build: .
|
|
6
|
-
container_name: {{kebabCase projectName}}-app
|
|
7
|
-
ports:
|
|
8
|
-
- "3000:3000"
|
|
9
|
-
environment:
|
|
10
|
-
- DATABASE_URL=postgresql://postgres:postgres@db:5432/{{snakeCase projectName}}
|
|
11
|
-
depends_on:
|
|
12
|
-
- db
|
|
13
|
-
|
|
14
4
|
db:
|
|
15
5
|
image: postgres:15-alpine
|
|
16
6
|
container_name: {{kebabCase projectName}}-db
|
|
@@ -19,7 +9,7 @@ services:
|
|
|
19
9
|
POSTGRES_PASSWORD: postgres
|
|
20
10
|
POSTGRES_DB: {{snakeCase projectName}}
|
|
21
11
|
ports:
|
|
22
|
-
- "
|
|
12
|
+
- "5434:5432"
|
|
23
13
|
volumes:
|
|
24
14
|
- postgres_data:/var/lib/postgresql/data
|
|
25
15
|
restart: unless-stopped
|
|
@@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
|
|
3
3
|
import { AuthModule } from './auth.module';
|
|
4
4
|
import { PaymentModule } from './payment.module';
|
|
5
5
|
import { PrismaService } from './infrastructure/database/prisma.service';
|
|
6
|
+
import { HealthController } from './infrastructure/http/health.controller';
|
|
6
7
|
|
|
7
8
|
@Module({
|
|
8
9
|
imports: [
|
|
@@ -12,7 +13,7 @@ import { PrismaService } from './infrastructure/database/prisma.service';
|
|
|
12
13
|
AuthModule,
|
|
13
14
|
PaymentModule,
|
|
14
15
|
],
|
|
15
|
-
controllers: [],
|
|
16
|
+
controllers: [HealthController],
|
|
16
17
|
providers: [PrismaService],
|
|
17
18
|
exports: [PrismaService],
|
|
18
19
|
})
|
|
@@ -6,85 +6,85 @@ import { StripeProvider } from '../infrastructure/stripe.provider';
|
|
|
6
6
|
|
|
7
7
|
@Injectable()
|
|
8
8
|
export class PaymentService {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
constructor(
|
|
10
|
+
private readonly userRepository: UserRepository,
|
|
11
|
+
private readonly stripeProvider: StripeProvider,
|
|
12
|
+
private readonly configService: ConfigService,
|
|
13
|
+
) {}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
async createCheckoutSession(userId: string, priceId: string) {
|
|
16
|
+
const user = await this.userRepository.findById(userId);
|
|
17
|
+
if (!user) throw new NotFoundException('User not found');
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
let customerId = user.stripeCustomerId;
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
if (!customerId) {
|
|
22
|
+
const customer = await this.stripeProvider.createCustomer(user.email, userId);
|
|
23
|
+
customerId = customer.id;
|
|
24
|
+
user.stripeCustomerId = customerId;
|
|
25
|
+
await this.userRepository.save(user);
|
|
26
|
+
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
return this.stripeProvider.createCheckoutSession(
|
|
29
|
+
customerId,
|
|
30
|
+
priceId,
|
|
31
|
+
userId,
|
|
32
|
+
`${this.configService.get('FRONTEND_URL')}/success?session_id={CHECKOUT_SESSION_ID}`,
|
|
33
|
+
`${this.configService.get('FRONTEND_URL')}/cancel`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
async createPortalSession(userId: string) {
|
|
38
|
+
const user = await this.userRepository.findById(userId);
|
|
39
|
+
if (!user?.stripeCustomerId) throw new NotFoundException('No Stripe customer found for this user');
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
return this.stripeProvider.createPortalSession(
|
|
42
|
+
user.stripeCustomerId,
|
|
43
|
+
`${this.configService.get('FRONTEND_URL')}/dashboard`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
46
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
async handleWebhook(payload: Buffer, signature: string) {
|
|
48
|
+
let event: Stripe.Event;
|
|
49
|
+
try {
|
|
50
|
+
event = this.stripeProvider.constructEvent(payload, signature);
|
|
51
|
+
} catch (err: any) {
|
|
52
|
+
throw new Error(`Webhook signature verification failed: ${err.message}`);
|
|
53
|
+
}
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
55
|
+
switch (event.type) {
|
|
56
|
+
case 'checkout.session.completed': {
|
|
57
|
+
const session = event.data.object as Stripe.Checkout.Session;
|
|
58
|
+
const userId = session.client_reference_id;
|
|
59
|
+
if (userId && session.customer) {
|
|
60
|
+
const user = await this.userRepository.findById(userId);
|
|
61
|
+
if (user) {
|
|
62
|
+
user.stripeCustomerId = session.customer as string;
|
|
63
|
+
await this.userRepository.save(user);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
console.log('Checkout completed for user:', userId);
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
case 'customer.subscription.updated': {
|
|
70
|
+
const sub = event.data.object as Stripe.Subscription;
|
|
71
|
+
console.log('Subscription updated:', sub.id, '| Status:', sub.status);
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
case 'customer.subscription.deleted': {
|
|
75
|
+
const sub = event.data.object as Stripe.Subscription;
|
|
76
|
+
console.log('Subscription deleted:', sub.id);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
case 'invoice.payment_failed': {
|
|
80
|
+
const invoice = event.data.object as Stripe.Invoice;
|
|
81
|
+
console.log('Payment failed for invoice:', invoice.id);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
console.log('Unhandled Stripe event:', event.type);
|
|
86
|
+
}
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
}
|
|
88
|
+
return { received: true };
|
|
90
89
|
}
|
|
90
|
+
}
|
|
@@ -4,9 +4,9 @@ export class User {
|
|
|
4
4
|
public readonly email: string,
|
|
5
5
|
public readonly name: string,
|
|
6
6
|
public readonly password: string,
|
|
7
|
-
public
|
|
7
|
+
public stripeCustomerId?: string,
|
|
8
8
|
public readonly createdAt: Date = new Date(),
|
|
9
9
|
) {
|
|
10
10
|
if (!email.includes('@')) throw new Error('Invalid email');
|
|
11
11
|
}
|
|
12
|
-
}
|
|
12
|
+
}
|
|
@@ -5,28 +5,28 @@ import { PrismaService } from '../prisma.service';
|
|
|
5
5
|
|
|
6
6
|
@Injectable()
|
|
7
7
|
export class PrismaUserRepository implements IUserRepository {
|
|
8
|
-
|
|
8
|
+
constructor(private prisma: PrismaService) {}
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
11
|
+
const user = await this.prisma.user.findUnique({ where: { email } });
|
|
12
|
+
if (!user) return null;
|
|
13
|
+
return new User(user.id, user.name, user.email, user.password);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async save(user: User): Promise<User> {
|
|
17
17
|
const savedUser = await this.prisma.user.upsert({
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
18
|
+
where: { email: user.email },
|
|
19
|
+
update: {
|
|
20
|
+
name: user.name,
|
|
21
|
+
password: user.password,
|
|
22
|
+
},
|
|
23
|
+
create: {
|
|
24
|
+
id: user.id,
|
|
25
|
+
name: user.name,
|
|
26
|
+
email: user.email,
|
|
27
|
+
password: user.password,
|
|
28
|
+
},
|
|
29
29
|
});
|
|
30
30
|
return new User(savedUser.id, savedUser.name, savedUser.email, savedUser.password);
|
|
31
|
-
|
|
32
|
-
}
|
|
31
|
+
}
|
|
32
|
+
}
|