@re-shell/cli
Version:
Full-stack development platform uniting microservices and microfrontends. Build complete applications with .NET (ASP.NET Core Web API, Minimal API), Java (Spring Boot, Quarkus, Micronaut, Vert.x), Rust (Actix-Web, Warp, Rocket, Axum), Python (FastAPI, Dja
1,493 lines (1,319 loc) • 53.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.MicronautGenerator = void 0;
const kotlin_base_generator_1 = require("./kotlin-base-generator");
const fs_1 = require("fs");
const path = __importStar(require("path"));
class MicronautGenerator extends kotlin_base_generator_1.KotlinBackendGenerator {
constructor() {
super('Micronaut');
}
getFrameworkPlugins() {
return `id("io.micronaut.application") version "4.2.0"
id("io.micronaut.aot") version "4.2.0"
kotlin("plugin.allopen") version "1.9.20"
kotlin("plugin.jpa") version "1.9.20"`;
}
getFrameworkDependencies() {
return `implementation("io.micronaut:micronaut-http-server-netty")
implementation("io.micronaut.kotlin:micronaut-kotlin-runtime")
implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
implementation("io.micronaut.security:micronaut-security-jwt")
implementation("io.micronaut.cache:micronaut-cache-caffeine")
implementation("io.micronaut.redis:micronaut-redis-lettuce")
implementation("io.micronaut.openapi:micronaut-openapi")
implementation("io.micronaut.graphql:micronaut-graphql")
implementation("io.micronaut.websocket:micronaut-websocket")
implementation("io.micronaut.kafka:micronaut-kafka")
implementation("io.micronaut.micrometer:micronaut-micrometer-core")
implementation("io.micronaut.micrometer:micronaut-micrometer-registry-prometheus")
implementation("io.micronaut.reactor:micronaut-reactor")
implementation("io.micronaut.reactor:micronaut-reactor-http-client")
implementation("io.micronaut.validation:micronaut-validation")
implementation("io.micronaut.email:micronaut-email-javamail")
implementation("io.micronaut.grpc:micronaut-grpc-server-runtime")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.annotation:jakarta.annotation-api")
implementation("at.favre.lib:bcrypt:0.10.2")
runtimeOnly("ch.qos.logback:logback-classic")
runtimeOnly("com.fasterxml.jackson.module:jackson-module-kotlin")
runtimeOnly("org.postgresql:postgresql")
runtimeOnly("com.h2database:h2")
compileOnly("io.micronaut.openapi:micronaut-openapi-annotations")
testImplementation("io.micronaut:micronaut-http-client")
testImplementation("io.micronaut.test:micronaut-test-junit5")
testImplementation("org.junit.jupiter:junit-jupiter-api")
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")`;
}
getFrameworkTasks() {
return `micronaut {
runtime("netty")
testRuntime("junit5")
processing {
incremental(true)
annotations("com.example.*")
}
aot {
optimizeServiceLoading = false
convertYamlToJava = false
precomputeOperations = true
cacheEnvironment = true
optimizeClassLoading = true
deduceEnvironment = true
optimizeNetty = true
}
}
graalvmNative {
toolchainDetection = false
}`;
}
async generateFrameworkFiles(projectPath, options) {
const basePackage = options.organization || 'com.example';
const srcDir = path.join(projectPath, 'src/main/kotlin', ...basePackage.split('.'));
await fs_1.promises.mkdir(srcDir, { recursive: true });
await this.generateApplication(srcDir, basePackage, options);
await this.generateControllers(srcDir, basePackage);
await this.generateServices(srcDir, basePackage);
await this.generateRepositories(srcDir, basePackage);
await this.generateEntities(srcDir, basePackage);
await this.generateDtos(srcDir, basePackage);
await this.generateSecurity(srcDir, basePackage);
await this.generateConfiguration(srcDir, basePackage);
await this.generateWebSocket(srcDir, basePackage);
await this.generateGraphQL(srcDir, basePackage);
await this.generateGrpc(srcDir, basePackage);
await this.generateUtils(srcDir, basePackage);
await this.generateResources(projectPath);
await this.generateTests(projectPath, basePackage);
}
async generateApplication(srcDir, basePackage, options) {
const appContent = `package ${basePackage}
import io.micronaut.runtime.Micronaut
import io.swagger.v3.oas.annotations.*
import io.swagger.v3.oas.annotations.info.*
@OpenAPIDefinition(
info = Info(
title = "${options.name}",
version = "1.0",
description = "Micronaut backend API",
license = License(name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0"),
contact = Contact(name = "Support", email = "support@example.com")
)
)
object Application {
@JvmStatic
fun main(args: Array<String>) {
Micronaut.run(Application::class.java, *args)
}
}`;
await fs_1.promises.writeFile(path.join(srcDir, 'Application.kt'), appContent);
}
async generateControllers(srcDir, basePackage) {
const controllerDir = path.join(srcDir, 'controller');
await fs_1.promises.mkdir(controllerDir, { recursive: true });
const userControllerContent = `package ${basePackage}.controller
import ${basePackage}.dto.*
import ${basePackage}.service.UserService
import io.micronaut.data.model.Page
import io.micronaut.data.model.Pageable
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.security.SecurityRequirement
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import reactor.core.publisher.Mono
@Controller("/api/v1/users")
@Tag(name = "Users", description = "User management APIs")
@SecurityRequirement(name = "bearer-key")
@Secured(SecurityRule.IS_AUTHENTICATED)
class UserController(private val userService: UserService) {
@Get
@Operation(summary = "Get all users")
@Secured("ADMIN")
fun getAllUsers(pageable: Pageable): Mono<Page<UserDto>> {
return userService.findAll(pageable)
}
@Get("/{id}")
@Operation(summary = "Get user by ID")
fun getUserById(@PathVariable id: Long): Mono<HttpResponse<UserDto>> {
return userService.findById(id)
.map { HttpResponse.ok(it) }
.switchIfEmpty(Mono.just(HttpResponse.notFound()))
}
@Get("/me")
@Operation(summary = "Get current user")
fun getCurrentUser(): Mono<UserDto> {
return userService.getCurrentUser()
}
@Put("/{id}")
@Operation(summary = "Update user")
fun updateUser(
@PathVariable id: Long,
@Valid @Body updateUserDto: UpdateUserDto
): Mono<HttpResponse<UserDto>> {
return userService.update(id, updateUserDto)
.map { HttpResponse.ok(it) }
.switchIfEmpty(Mono.just(HttpResponse.notFound()))
}
@Delete("/{id}")
@Operation(summary = "Delete user")
@Secured("ADMIN")
@Status(HttpStatus.NO_CONTENT)
fun deleteUser(@PathVariable id: Long): Mono<HttpResponse<Void>> {
return userService.delete(id)
.map { HttpResponse.noContent<Void>() }
}
}`;
await fs_1.promises.writeFile(path.join(controllerDir, 'UserController.kt'), userControllerContent);
const authControllerContent = `package ${basePackage}.controller
import ${basePackage}.dto.*
import ${basePackage}.service.AuthService
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.*
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import jakarta.validation.Valid
import reactor.core.publisher.Mono
@Controller("/api/v1/auth")
@Tag(name = "Authentication", description = "Authentication APIs")
@Secured(SecurityRule.IS_ANONYMOUS)
class AuthController(private val authService: AuthService) {
@Post("/register")
@Operation(summary = "Register new user")
@Status(HttpStatus.CREATED)
fun register(@Valid @Body registerDto: RegisterDto): Mono<TokenDto> {
return authService.register(registerDto)
}
@Post("/login")
@Operation(summary = "Login user")
fun login(@Valid @Body loginDto: LoginDto): Mono<TokenDto> {
return authService.login(loginDto)
}
@Post("/refresh")
@Operation(summary = "Refresh access token")
fun refresh(@Valid @Body refreshTokenDto: RefreshTokenDto): Mono<TokenDto> {
return authService.refresh(refreshTokenDto)
}
@Post("/logout")
@Operation(summary = "Logout user")
@Status(HttpStatus.NO_CONTENT)
@Secured(SecurityRule.IS_AUTHENTICATED)
fun logout(@Header("Authorization") token: String): Mono<HttpResponse<Void>> {
return authService.logout(token)
.map { HttpResponse.noContent() }
}
}`;
await fs_1.promises.writeFile(path.join(controllerDir, 'AuthController.kt'), authControllerContent);
const healthControllerContent = `package ${basePackage}.controller
import io.micronaut.health.HealthStatus
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.management.health.indicator.HealthIndicator
import io.micronaut.management.health.indicator.HealthResult
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
import reactor.core.publisher.Mono
import jakarta.inject.Singleton
@Controller("/api/v1/health")
@Tag(name = "Health", description = "Health check APIs")
class HealthController {
@Get
@Operation(summary = "Health check")
fun health(): Map<String, Any> {
return mapOf(
"status" to "UP",
"timestamp" to System.currentTimeMillis(),
"service" to "micronaut-api"
)
}
}
@Singleton
class CustomHealthIndicator : HealthIndicator {
override fun getResult(): Mono<HealthResult> {
return Mono.just(
HealthResult.builder("custom")
.status(HealthStatus.UP)
.details(mapOf("service" to "micronaut-api"))
.build()
)
}
}`;
await fs_1.promises.writeFile(path.join(controllerDir, 'HealthController.kt'), healthControllerContent);
}
async generateServices(srcDir, basePackage) {
const serviceDir = path.join(srcDir, 'service');
await fs_1.promises.mkdir(serviceDir, { recursive: true });
const userServiceContent = `package ${basePackage}.service
import ${basePackage}.dto.*
import ${basePackage}.entity.User
import ${basePackage}.repository.UserRepository
import ${basePackage}.security.SecurityUtils
import io.micronaut.cache.annotation.CacheInvalidate
import io.micronaut.cache.annotation.Cacheable
import io.micronaut.data.model.Page
import io.micronaut.data.model.Pageable
import jakarta.inject.Singleton
import jakarta.transaction.Transactional
import reactor.core.publisher.Mono
@Singleton
@Transactional
open class UserService(
private val userRepository: UserRepository,
private val securityUtils: SecurityUtils
) {
@Cacheable("users")
open fun findAll(pageable: Pageable): Mono<Page<UserDto>> {
return Mono.fromCallable {
userRepository.findAll(pageable).map { it.toDto() }
}
}
@Cacheable("users", key = "#id")
open fun findById(id: Long): Mono<UserDto?> {
return Mono.fromCallable {
userRepository.findById(id).orElse(null)?.toDto()
}
}
open fun findByEmail(email: String): Mono<User?> {
return Mono.fromCallable {
userRepository.findByEmail(email).orElse(null)
}
}
open fun getCurrentUser(): Mono<UserDto> {
return securityUtils.getCurrentUserEmail()
.flatMap { email -> findByEmail(email) }
.map { it?.toDto() ?: throw IllegalStateException("Current user not found") }
}
@CacheInvalidate("users", key = "#id")
open fun update(id: Long, updateUserDto: UpdateUserDto): Mono<UserDto?> {
return Mono.fromCallable {
userRepository.findById(id).orElse(null)?.let { user ->
updateUserDto.name?.let { user.name = it }
updateUserDto.email?.let { user.email = it }
userRepository.update(user).toDto()
}
}
}
@CacheInvalidate("users", key = "#id")
open fun delete(id: Long): Mono<Unit> {
return Mono.fromCallable {
userRepository.deleteById(id)
}
}
private fun User.toDto(): UserDto = UserDto(
id = this.id!!,
email = this.email,
name = this.name,
roles = this.roles.map { it.name },
createdAt = this.createdAt,
updatedAt = this.updatedAt
)
}`;
await fs_1.promises.writeFile(path.join(serviceDir, 'UserService.kt'), userServiceContent);
const authServiceContent = `package ${basePackage}.service
import ${basePackage}.dto.*
import ${basePackage}.entity.User
import ${basePackage}.entity.Role
import ${basePackage}.repository.UserRepository
import ${basePackage}.repository.RoleRepository
import ${basePackage}.security.JwtTokenProvider
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.authentication.AuthenticationException
import io.micronaut.security.authentication.AuthenticationRequest
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.Authenticator
import jakarta.inject.Singleton
import jakarta.transaction.Transactional
import reactor.core.publisher.Mono
import java.time.LocalDateTime
import at.favre.lib.crypto.bcrypt.BCrypt
@Singleton
@Transactional
open class AuthService(
private val userRepository: UserRepository,
private val roleRepository: RoleRepository,
private val jwtTokenProvider: JwtTokenProvider
) : Authenticator<AuthenticationRequest<String, String>, AuthenticationResponse> {
open fun register(registerDto: RegisterDto): Mono<TokenDto> {
return Mono.fromCallable {
if (userRepository.existsByEmail(registerDto.email)) {
throw IllegalArgumentException("Email already registered")
}
val userRole = roleRepository.findByName("ROLE_USER").orElseThrow {
IllegalStateException("Default role not found")
}
val user = User(
email = registerDto.email,
password = BCrypt.withDefaults().hashToString(12, registerDto.password.toCharArray()),
name = registerDto.name,
roles = mutableSetOf(userRole),
isEnabled = true,
createdAt = LocalDateTime.now(),
updatedAt = LocalDateTime.now()
)
userRepository.save(user)
generateTokens(user)
}
}
open fun login(loginDto: LoginDto): Mono<TokenDto> {
return authenticate(
AuthenticationRequest.build(loginDto.email, loginDto.password)
).map { auth ->
val user = userRepository.findByEmail(auth.name).orElseThrow()
generateTokens(user)
}
}
open fun refresh(refreshTokenDto: RefreshTokenDto): Mono<TokenDto> {
return Mono.fromCallable {
val email = jwtTokenProvider.validateTokenAndGetEmail(refreshTokenDto.refreshToken)
val user = userRepository.findByEmail(email).orElseThrow {
IllegalArgumentException("User not found")
}
generateTokens(user)
}
}
open fun logout(token: String): Mono<Unit> {
return Mono.fromCallable {
jwtTokenProvider.invalidateToken(token)
}
}
override fun authenticate(
httpRequest: AuthenticationRequest<String, String>?
): Mono<AuthenticationResponse> {
return Mono.fromCallable {
val email = httpRequest?.identity ?: throw AuthenticationException("Invalid credentials")
val password = httpRequest.secret ?: throw AuthenticationException("Invalid credentials")
val user = userRepository.findByEmail(email).orElseThrow {
AuthenticationException("Invalid credentials")
}
if (!BCrypt.verifyer().verify(password.toCharArray(), user.password).verified) {
throw AuthenticationException("Invalid credentials")
}
AuthenticationResponse.success(
user.email,
user.roles.map { it.name }
)
}
}
private fun generateTokens(user: User): TokenDto {
val accessToken = jwtTokenProvider.createAccessToken(user.email, user.roles.map { it.name })
val refreshToken = jwtTokenProvider.createRefreshToken(user.email)
return TokenDto(
accessToken = accessToken,
refreshToken = refreshToken,
tokenType = "Bearer",
expiresIn = jwtTokenProvider.accessTokenValidityInMilliseconds / 1000
)
}
}`;
await fs_1.promises.writeFile(path.join(serviceDir, 'AuthService.kt'), authServiceContent);
}
async generateRepositories(srcDir, basePackage) {
const repoDir = path.join(srcDir, 'repository');
await fs_1.promises.mkdir(repoDir, { recursive: true });
const userRepoContent = `package ${basePackage}.repository
import ${basePackage}.entity.User
import io.micronaut.data.annotation.Query
import io.micronaut.data.annotation.Repository
import io.micronaut.data.jpa.repository.JpaRepository
import java.util.Optional
@Repository
interface UserRepository : JpaRepository<User, Long> {
fun findByEmail(email: String): Optional<User>
fun existsByEmail(email: String): Boolean
@Query("SELECT u FROM User u LEFT JOIN FETCH u.roles WHERE u.email = :email")
fun findByEmailWithRoles(email: String): Optional<User>
@Query("SELECT u FROM User u WHERE u.isEnabled = true")
fun findAllActive(): List<User>
}`;
await fs_1.promises.writeFile(path.join(repoDir, 'UserRepository.kt'), userRepoContent);
const roleRepoContent = `package ${basePackage}.repository
import ${basePackage}.entity.Role
import io.micronaut.data.annotation.Repository
import io.micronaut.data.jpa.repository.JpaRepository
import java.util.Optional
@Repository
interface RoleRepository : JpaRepository<Role, Long> {
fun findByName(name: String): Optional<Role>
}`;
await fs_1.promises.writeFile(path.join(repoDir, 'RoleRepository.kt'), roleRepoContent);
}
async generateEntities(srcDir, basePackage) {
const entityDir = path.join(srcDir, 'entity');
await fs_1.promises.mkdir(entityDir, { recursive: true });
const userEntityContent = `package ${basePackage}.entity
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import io.micronaut.security.authentication.Authentication
import jakarta.persistence.*
import java.security.Principal
import java.time.LocalDateTime
@MappedEntity("users")
@Entity
@Table(name = "users")
data class User(
@field:Id
@field:GeneratedValue(GeneratedValue.Type.IDENTITY)
val id: Long? = null,
@Column(unique = true, nullable = false)
var email: String,
@Column(nullable = false)
var password: String,
@Column(nullable = false)
var name: String,
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = [JoinColumn(name = "user_id")],
inverseJoinColumns = [JoinColumn(name = "role_id")]
)
var roles: MutableSet<Role> = mutableSetOf(),
@Column(nullable = false)
var isEnabled: Boolean = true,
@Column(nullable = false)
var createdAt: LocalDateTime = LocalDateTime.now(),
@Column(nullable = false)
var updatedAt: LocalDateTime = LocalDateTime.now()
) : Principal {
override fun getName(): String = email
@PreUpdate
fun preUpdate() {
updatedAt = LocalDateTime.now()
}
}`;
await fs_1.promises.writeFile(path.join(entityDir, 'User.kt'), userEntityContent);
const roleEntityContent = `package ${basePackage}.entity
import io.micronaut.data.annotation.GeneratedValue
import io.micronaut.data.annotation.Id
import io.micronaut.data.annotation.MappedEntity
import jakarta.persistence.*
@MappedEntity("roles")
@Entity
@Table(name = "roles")
data class Role(
@field:Id
@field:GeneratedValue(GeneratedValue.Type.IDENTITY)
val id: Long? = null,
@Column(unique = true, nullable = false)
val name: String,
@Column
val description: String? = null
)`;
await fs_1.promises.writeFile(path.join(entityDir, 'Role.kt'), roleEntityContent);
}
async generateDtos(srcDir, basePackage) {
const dtoDir = path.join(srcDir, 'dto');
await fs_1.promises.mkdir(dtoDir, { recursive: true });
const userDtoContent = `package ${basePackage}.dto
import com.fasterxml.jackson.annotation.JsonFormat
import io.micronaut.core.annotation.Introspected
import java.time.LocalDateTime
@Introspected
data class UserDto(
val id: Long,
val email: String,
val name: String,
val roles: List<String>,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val createdAt: LocalDateTime,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val updatedAt: LocalDateTime
)
@Introspected
data class CreateUserDto(
val email: String,
val password: String,
val name: String
)
@Introspected
data class UpdateUserDto(
val name: String? = null,
val email: String? = null
)`;
await fs_1.promises.writeFile(path.join(dtoDir, 'UserDto.kt'), userDtoContent);
const authDtoContent = `package ${basePackage}.dto
import io.micronaut.core.annotation.Introspected
import jakarta.validation.constraints.Email
import jakarta.validation.constraints.NotBlank
import jakarta.validation.constraints.Size
@Introspected
data class RegisterDto(
@field:NotBlank(message = "Email is required")
@field:Email(message = "Invalid email format")
val email: String,
@field:NotBlank(message = "Password is required")
@field:Size(min = 8, message = "Password must be at least 8 characters")
val password: String,
@field:NotBlank(message = "Name is required")
val name: String
)
@Introspected
data class LoginDto(
@field:NotBlank(message = "Email is required")
@field:Email(message = "Invalid email format")
val email: String,
@field:NotBlank(message = "Password is required")
val password: String
)
@Introspected
data class TokenDto(
val accessToken: String,
val refreshToken: String,
val tokenType: String = "Bearer",
val expiresIn: Long
)
@Introspected
data class RefreshTokenDto(
@field:NotBlank(message = "Refresh token is required")
val refreshToken: String
)`;
await fs_1.promises.writeFile(path.join(dtoDir, 'AuthDto.kt'), authDtoContent);
const errorDtoContent = `package ${basePackage}.dto
import com.fasterxml.jackson.annotation.JsonFormat
import io.micronaut.core.annotation.Introspected
import java.time.LocalDateTime
@Introspected
data class ErrorDto(
val status: Int,
val error: String,
val message: String?,
val path: String,
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
val timestamp: LocalDateTime = LocalDateTime.now()
)
@Introspected
data class ValidationErrorDto(
val field: String,
val message: String
)
@Introspected
data class ApiResponse<T>(
val success: Boolean,
val data: T? = null,
val error: String? = null,
val timestamp: Long = System.currentTimeMillis()
)`;
await fs_1.promises.writeFile(path.join(dtoDir, 'ErrorDto.kt'), errorDtoContent);
}
async generateSecurity(srcDir, basePackage) {
const securityDir = path.join(srcDir, 'security');
await fs_1.promises.mkdir(securityDir, { recursive: true });
const jwtTokenProviderContent = `package ${basePackage}.security
import io.jsonwebtoken.*
import io.jsonwebtoken.security.Keys
import io.micronaut.context.annotation.Value
import jakarta.inject.Singleton
import java.util.*
import javax.crypto.SecretKey
@Singleton
class JwtTokenProvider {
@Value("\\\${micronaut.security.token.jwt.generator.secret}")
private lateinit var jwtSecret: String
@Value("\\\${app.jwt.access-token-expiration:3600000}")
val accessTokenValidityInMilliseconds: Long = 3600000
@Value("\\\${app.jwt.refresh-token-expiration:2592000000}")
private val refreshTokenValidityInMilliseconds: Long = 2592000000
private val key: SecretKey by lazy {
Keys.hmacShaKeyFor(jwtSecret.toByteArray())
}
fun createAccessToken(email: String, roles: List<String>): String {
val now = Date()
val validity = Date(now.time + accessTokenValidityInMilliseconds)
return Jwts.builder()
.setSubject(email)
.claim("roles", roles)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key)
.compact()
}
fun createRefreshToken(email: String): String {
val now = Date()
val validity = Date(now.time + refreshTokenValidityInMilliseconds)
return Jwts.builder()
.setSubject(email)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key)
.compact()
}
fun validateTokenAndGetEmail(token: String): String {
return try {
val claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.body
claims.subject
} catch (e: JwtException) {
throw IllegalArgumentException("Invalid JWT token", e)
}
}
fun invalidateToken(token: String) {
// Add to Redis blacklist
}
}`;
await fs_1.promises.writeFile(path.join(securityDir, 'JwtTokenProvider.kt'), jwtTokenProviderContent);
const authenticationProviderContent = `package ${basePackage}.security
import io.micronaut.http.HttpRequest
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.authentication.AuthenticationProvider
import io.micronaut.security.authentication.AuthenticationRequest
import io.micronaut.security.authentication.AuthenticationResponse
import io.micronaut.security.authentication.AuthenticationFailed
import io.micronaut.security.authentication.AuthenticationFailureReason
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import ${basePackage}.repository.UserRepository
import at.favre.lib.crypto.bcrypt.BCrypt
@Singleton
class AuthenticationProviderUserPassword(
private val userRepository: UserRepository
) : AuthenticationProvider<HttpRequest<*>, String, String> {
override fun authenticate(
httpRequest: HttpRequest<*>?,
authenticationRequest: AuthenticationRequest<String, String>
): Mono<AuthenticationResponse> {
return Mono.create { emitter ->
val email = authenticationRequest.identity
val password = authenticationRequest.secret
userRepository.findByEmail(email).ifPresentOrElse({ user ->
if (BCrypt.verifyer().verify(password.toCharArray(), user.password).verified && user.isEnabled) {
emitter.success(
AuthenticationResponse.success(
user.email,
user.roles.map { it.name }
)
)
} else {
emitter.error(
AuthenticationFailed(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH)
)
}
}) {
emitter.error(
AuthenticationFailed(AuthenticationFailureReason.USER_NOT_FOUND)
)
}
}
}
}`;
await fs_1.promises.writeFile(path.join(securityDir, 'AuthenticationProvider.kt'), authenticationProviderContent);
const securityUtilsContent = `package ${basePackage}.security
import io.micronaut.security.authentication.Authentication
import io.micronaut.security.utils.SecurityService
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
@Singleton
class SecurityUtils(
private val securityService: SecurityService
) {
fun getCurrentUserEmail(): Mono<String> {
return Mono.fromCallable {
securityService.authentication()
.map { it.name }
.orElseThrow { IllegalStateException("User not authenticated") }
}
}
fun isAuthenticated(): Boolean {
return securityService.isAuthenticated
}
fun hasRole(role: String): Boolean {
return securityService.authentication()
.map { auth ->
auth.roles.contains(role)
}
.orElse(false)
}
}`;
await fs_1.promises.writeFile(path.join(securityDir, 'SecurityUtils.kt'), securityUtilsContent);
}
async generateConfiguration(srcDir, basePackage) {
const configDir = path.join(srcDir, 'config');
await fs_1.promises.mkdir(configDir, { recursive: true });
const corsConfigContent = `package ${basePackage}.config
import io.micronaut.context.annotation.ConfigurationProperties
import io.micronaut.context.annotation.Requires
import io.micronaut.core.annotation.Nullable
import io.micronaut.http.HttpMethod
@ConfigurationProperties("app.cors")
@Requires(property = "app.cors.enabled", value = "true")
class CorsConfiguration {
var enabled: Boolean = true
@Nullable
var allowedOrigins: List<String>? = null
var allowedMethods: List<HttpMethod> = listOf(
HttpMethod.GET,
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.DELETE,
HttpMethod.OPTIONS
)
var allowedHeaders: List<String> = listOf("*")
var exposedHeaders: List<String> = listOf()
var allowCredentials: Boolean = true
var maxAge: Long = 3600
}`;
await fs_1.promises.writeFile(path.join(configDir, 'CorsConfiguration.kt'), corsConfigContent);
const dataInitializerContent = `package ${basePackage}.config
import ${basePackage}.entity.Role
import ${basePackage}.repository.RoleRepository
import io.micronaut.context.annotation.Requires
import io.micronaut.context.event.StartupEvent
import io.micronaut.runtime.event.annotation.EventListener
import jakarta.inject.Singleton
import jakarta.transaction.Transactional
@Singleton
@Requires(property = "app.data.initialize", value = "true", defaultValue = "false")
open class DataInitializer(
private val roleRepository: RoleRepository
) {
@EventListener
@Transactional
open fun onStartup(event: StartupEvent) {
initializeRoles()
}
private fun initializeRoles() {
val roles = listOf(
Role(name = "ROLE_USER", description = "Default user role"),
Role(name = "ROLE_ADMIN", description = "Administrator role"),
Role(name = "ROLE_MODERATOR", description = "Moderator role")
)
roles.forEach { role ->
if (!roleRepository.findByName(role.name).isPresent) {
roleRepository.save(role)
}
}
}
}`;
await fs_1.promises.writeFile(path.join(configDir, 'DataInitializer.kt'), dataInitializerContent);
}
async generateWebSocket(srcDir, basePackage) {
const wsDir = path.join(srcDir, 'websocket');
await fs_1.promises.mkdir(wsDir, { recursive: true });
const wsServerContent = `package ${basePackage}.websocket
import io.micronaut.websocket.WebSocketBroadcaster
import io.micronaut.websocket.WebSocketSession
import io.micronaut.websocket.annotation.OnClose
import io.micronaut.websocket.annotation.OnMessage
import io.micronaut.websocket.annotation.OnOpen
import io.micronaut.websocket.annotation.ServerWebSocket
import reactor.core.publisher.Mono
import java.util.concurrent.ConcurrentHashMap
@ServerWebSocket("/ws/chat/{topic}/{username}")
class ChatWebSocket(
private val broadcaster: WebSocketBroadcaster
) {
private val sessions = ConcurrentHashMap<String, WebSocketSession>()
@OnOpen
fun onOpen(topic: String, username: String, session: WebSocketSession): Mono<Void> {
return Mono.fromCallable {
sessions[session.id] = session
broadcaster.broadcastSync(
ChatMessage(
from = "System",
content = "$username joined the chat",
topic = topic
),
isValid(topic)
)
}.then()
}
@OnMessage
fun onMessage(
topic: String,
username: String,
message: String,
session: WebSocketSession
): Mono<Void> {
return Mono.fromCallable {
broadcaster.broadcastSync(
ChatMessage(
from = username,
content = message,
topic = topic
),
isValid(topic)
)
}.then()
}
@OnClose
fun onClose(
topic: String,
username: String,
session: WebSocketSession
): Mono<Void> {
return Mono.fromCallable {
sessions.remove(session.id)
broadcaster.broadcastSync(
ChatMessage(
from = "System",
content = "$username left the chat",
topic = topic
),
isValid(topic)
)
}.then()
}
private fun isValid(topic: String): (WebSocketSession) -> Boolean = { session ->
session.uriVariables["topic"] == topic
}
}
data class ChatMessage(
val from: String,
val content: String,
val topic: String,
val timestamp: Long = System.currentTimeMillis()
)`;
await fs_1.promises.writeFile(path.join(wsDir, 'ChatWebSocket.kt'), wsServerContent);
}
async generateGraphQL(srcDir, basePackage) {
const graphqlDir = path.join(srcDir, 'graphql');
await fs_1.promises.mkdir(graphqlDir, { recursive: true });
const queryResolverContent = `package ${basePackage}.graphql
import ${basePackage}.dto.UserDto
import ${basePackage}.service.UserService
import graphql.kickstart.tools.GraphQLQueryResolver
import io.micronaut.data.model.PageRequest
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
import reactor.core.publisher.Flux
@Singleton
@Secured(SecurityRule.IS_AUTHENTICATED)
class UserQueryResolver(
private val userService: UserService
) : GraphQLQueryResolver {
fun user(id: Long): Mono<UserDto?> {
return userService.findById(id)
}
@Secured("ADMIN")
fun users(page: Int = 0, size: Int = 10): Flux<UserDto> {
return userService.findAll(PageRequest.of(page, size))
.flatMapMany { Flux.fromIterable(it.content) }
}
fun me(): Mono<UserDto> {
return userService.getCurrentUser()
}
}`;
await fs_1.promises.writeFile(path.join(graphqlDir, 'UserQueryResolver.kt'), queryResolverContent);
const mutationResolverContent = `package ${basePackage}.graphql
import ${basePackage}.dto.*
import ${basePackage}.service.AuthService
import ${basePackage}.service.UserService
import graphql.kickstart.tools.GraphQLMutationResolver
import io.micronaut.security.annotation.Secured
import io.micronaut.security.rules.SecurityRule
import jakarta.inject.Singleton
import reactor.core.publisher.Mono
@Singleton
class UserMutationResolver(
private val authService: AuthService,
private val userService: UserService
) : GraphQLMutationResolver {
@Secured(SecurityRule.IS_ANONYMOUS)
fun register(input: RegisterDto): Mono<TokenDto> {
return authService.register(input)
}
@Secured(SecurityRule.IS_ANONYMOUS)
fun login(input: LoginDto): Mono<TokenDto> {
return authService.login(input)
}
@Secured(SecurityRule.IS_AUTHENTICATED)
fun updateUser(id: Long, input: UpdateUserDto): Mono<UserDto?> {
return userService.update(id, input)
}
}`;
await fs_1.promises.writeFile(path.join(graphqlDir, 'UserMutationResolver.kt'), mutationResolverContent);
const graphqlFactoryContent = `package ${basePackage}.graphql
import graphql.GraphQL
import graphql.kickstart.tools.SchemaParser
import io.micronaut.context.annotation.Factory
import io.micronaut.core.io.ResourceResolver
import jakarta.inject.Singleton
@Factory
class GraphQLFactory {
@Singleton
fun graphQL(
resourceResolver: ResourceResolver,
userQueryResolver: UserQueryResolver,
userMutationResolver: UserMutationResolver
): GraphQL {
val schemaParser = SchemaParser.newParser()
.file("schema.graphqls")
.resolvers(userQueryResolver, userMutationResolver)
.build()
return GraphQL.newGraphQL(schemaParser.makeExecutableSchema())
.build()
}
}`;
await fs_1.promises.writeFile(path.join(graphqlDir, 'GraphQLFactory.kt'), graphqlFactoryContent);
}
async generateGrpc(srcDir, basePackage) {
const grpcDir = path.join(srcDir, 'grpc');
await fs_1.promises.mkdir(grpcDir, { recursive: true });
const userGrpcServiceContent = `package ${basePackage}.grpc
import ${basePackage}.dto.UserDto
import ${basePackage}.service.UserService
import io.grpc.Status
import io.grpc.StatusException
import io.grpc.stub.StreamObserver
import io.micronaut.grpc.annotation.GrpcService
import jakarta.inject.Inject
@GrpcService
class UserGrpcService : UserServiceGrpc.UserServiceImplBase() {
@Inject
lateinit var userService: UserService
override fun getUser(
request: GetUserRequest,
responseObserver: StreamObserver<UserResponse>
) {
userService.findById(request.id)
.subscribe(
{ user ->
user?.let {
responseObserver.onNext(it.toGrpcResponse())
responseObserver.onCompleted()
} ?: run {
responseObserver.onError(
StatusException(Status.NOT_FOUND.withDescription("User not found"))
)
}
},
{ error ->
responseObserver.onError(
StatusException(Status.INTERNAL.withDescription(error.message))
)
}
)
}
override fun listUsers(
request: ListUsersRequest,
responseObserver: StreamObserver<UserResponse>
) {
userService.findAll(
io.micronaut.data.model.PageRequest.of(request.page, request.size)
).subscribe(
{ page ->
page.content.forEach { user ->
responseObserver.onNext(user.toGrpcResponse())
}
responseObserver.onCompleted()
},
{ error ->
responseObserver.onError(
StatusException(Status.INTERNAL.withDescription(error.message))
)
}
)
}
private fun UserDto.toGrpcResponse(): UserResponse {
return UserResponse.newBuilder()
.setId(id)
.setEmail(email)
.setName(name)
.addAllRoles(roles)
.setCreatedAt(createdAt.toString())
.setUpdatedAt(updatedAt.toString())
.build()
}
}`;
await fs_1.promises.writeFile(path.join(grpcDir, 'UserGrpcService.kt'), userGrpcServiceContent);
}
async generateUtils(srcDir, basePackage) {
const utilsDir = path.join(srcDir, 'utils');
await fs_1.promises.mkdir(utilsDir, { recursive: true });
const validationUtilsContent = `package ${basePackage}.utils
object ValidationUtils {
fun isValidEmail(email: String): Boolean {
val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\$".toRegex()
return email.matches(emailRegex)
}
fun isStrongPassword(password: String): Boolean {
return password.length >= 8 &&
password.any { it.isUpperCase() } &&
password.any { it.isLowerCase() } &&
password.any { it.isDigit() } &&
password.any { !it.isLetterOrDigit() }
}
fun sanitizeInput(input: String): String {
return input.trim()
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'")
.replace("&", "&")
}
}`;
await fs_1.promises.writeFile(path.join(utilsDir, 'ValidationUtils.kt'), validationUtilsContent);
const errorHandlerContent = `package ${basePackage}.utils
import ${basePackage}.dto.ErrorDto
import io.micronaut.context.annotation.Requires
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.http.HttpStatus
import io.micronaut.http.annotation.Produces
import io.micronaut.http.server.exceptions.ExceptionHandler
import jakarta.inject.Singleton
@Produces
@Singleton
@Requires(classes = [IllegalArgumentException::class, ExceptionHandler::class])
class IllegalArgumentExceptionHandler : ExceptionHandler<IllegalArgumentException, HttpResponse<ErrorDto>> {
override fun handle(request: HttpRequest<*>, exception: IllegalArgumentException): HttpResponse<ErrorDto> {
return HttpResponse.badRequest(
ErrorDto(
status = HttpStatus.BAD_REQUEST.code,
error = "Bad Request",
message = exception.message,
path = request.path
)
)
}
}
@Produces
@Singleton
@Requires(classes = [NoSuchElementException::class, ExceptionHandler::class])
class NotFoundExceptionHandler : ExceptionHandler<NoSuchElementException, HttpResponse<ErrorDto>> {
override fun handle(request: HttpRequest<*>, exception: NoSuchElementException): HttpResponse<ErrorDto> {
return HttpResponse.notFound(
ErrorDto(
status = HttpStatus.NOT_FOUND.code,
error = "Not Found",
message = exception.message ?: "Resource not found",
path = request.path
)
)
}
}`;
await fs_1.promises.writeFile(path.join(utilsDir, 'ErrorHandler.kt'), errorHandlerContent);
}
async generateResources(projectPath) {
const resourcesDir = path.join(projectPath, 'src/main/resources');
await fs_1.promises.mkdir(resourcesDir, { recursive: true });
const applicationYaml = `micronaut:
application:
name: micronaut-api
server:
port: \${PORT:8080}
cors:
enabled: true
configurations:
web:
allowedOrigins:
- http://localhost:3000
- http://localhost:4200
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowedHeaders:
- Content-Type
- Authorization
exposedHeaders:
- Content-Type
- Authorization
allowCredentials: true
maxAge: 3600
security:
enabled: true
endpoints:
login:
enabled: false
oauth:
enabled: false
token:
jwt:
enabled: true
signatures:
secret:
generator:
secret: \${JWT_SECRET:your-secret-key-here-please-change-in-production}
generator:
access-token:
expiration: 3600
refresh-token:
enabled: true
expiration: 2592000
intercept-url-map:
- pattern: /swagger/**
access:
- isAnonymous()
- pattern: /api/v1/auth/**
access:
- isAnonymous()
- pattern: /api/v1/health/**
access:
- isAnonymous()
metrics:
enabled: true
export:
prometheus:
enabled: true
step: PT1M
descriptions: true
router:
static-resources:
swagger:
paths: classpath:META-INF/swagger
mapping: /swagger/**
datasources:
default:
url: jdbc:postgresql://\${DB_HOST:localhost}:\${DB_PORT:5432}/\${DB_NAME:app_db}
driverClassName: org.postgresql.Driver
username: \${DB_USER:postgres}
password: \${DB_PASSWORD:postgres}
dialect: POSTGRES
schema-generate: UPDATE
jpa:
default:
properties:
hibernate:
hbm2ddl:
auto: update
show_sql: false
format_sql: true
redis:
uri: redis://\${REDIS_HOST:localhost}:\${REDIS_PORT:6379}
password: \${REDIS_PASSWORD:}
kafka:
bootstrap:
servers: \${KAFKA_SERVERS:localhost:9092}
graphql:
enabled: true
path: /graphql
graphiql:
enabled: true
path: /graphiql
grpc:
server:
port: 50051
keep-alive-time: 30s
jackson:
serialization:
write-dates-as-timestamps: false
app:
data:
initialize: true
jwt:
access-token-expiration: 3600000
refresh-token-expiration: 2592000000`;
await fs_1.promises.writeFile(path.join(resourcesDir, 'application.yml'), applicationYaml);
const logbackContent = `<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="com.example" level="DEBUG"/>
</configuration>`;
await fs_1.promises.writeFile(path.join(resourcesDir, 'logback.xml'), logbackContent);
const graphqlSchema = `type Query {
user(id: ID!): User
users(page: Int = 0, size: Int = 10): [User!]!
me: User!
}
type Mutation {
register(input: RegisterInput!): AuthToken!
login(input: LoginInput!): AuthToken!
updateUser(id: ID!, input: UpdateUserInput!): User
}
type User {
id: ID!
email: String!
name: String!
roles: [String!]!
createdAt: String!
updatedAt: String!
}
type AuthToken {
accessToken: String!
refreshToken: String!
tokenType: String!
expiresIn: Int!
}
input RegisterInput {
email: String!
password: String!
name: String!
}
input LoginInput {
email: String!
password: String!
}
input UpdateUserInput {
name: String
email: String
}`;
await fs_1.promises.writeFile(path.join(resourcesDir, 'schema.graphqls'), graphqlSchema);
}
async generateTests(projectPath, basePackage) {
const testDir = path.join(projectPath, 'src/test/kotlin', ...basePackage.split('.'));
await fs_1.promises.mkdir(testDir, { recursive: true });
const userControllerTestContent = `package ${basePackage}.controller
import ${basePackage}.dto.*
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.annotation.Client
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import jakarta.inject.Inject
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
@MicronautTest
class UserControllerTest {
@Inject
@field:Client("/")
lateinit var client: HttpClient
@Test
fun testHealthEndpoint() {
val request = HttpRequest.GET<Map<String, Any>>("/api/v1/health")
val response = client.toBlocking().exchange(request, Map::class.java)
assertEquals(HttpStatus.OK, response.status)
val body = response