sprygen 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +80 -0
  2. package/dist/cli.js +55 -0
  3. package/package.json +53 -0
  4. package/templates/auth/AuthController.java.ejs +40 -0
  5. package/templates/auth/JwtAuthFilter.java.ejs +62 -0
  6. package/templates/auth/JwtService.java.ejs +81 -0
  7. package/templates/auth/SecurityConfig.java.ejs +65 -0
  8. package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
  9. package/templates/entity/Entity.java.ejs +40 -0
  10. package/templates/entity/EntityController.java.ejs +92 -0
  11. package/templates/entity/EntityControllerTest.java.ejs +24 -0
  12. package/templates/entity/EntityDto.java.ejs +32 -0
  13. package/templates/entity/EntityRepository.java.ejs +9 -0
  14. package/templates/entity/EntityService.java.ejs +32 -0
  15. package/templates/project/java/config/CorsConfig.java.ejs +24 -0
  16. package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
  17. package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
  18. package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
  19. package/templates/project/java/controller/AdminController.java.ejs +82 -0
  20. package/templates/project/java/controller/AuthController.java.ejs +86 -0
  21. package/templates/project/java/controller/HomeController.java.ejs +63 -0
  22. package/templates/project/java/controller/ProfileController.java.ejs +65 -0
  23. package/templates/project/java/controller/UserController.java.ejs +35 -0
  24. package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
  25. package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
  26. package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
  27. package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
  28. package/templates/project/java/dto/UserDto.java.ejs +17 -0
  29. package/templates/project/java/entity/Role.java.ejs +6 -0
  30. package/templates/project/java/entity/User.java.ejs +97 -0
  31. package/templates/project/java/repository/UserRepository.java.ejs +11 -0
  32. package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
  33. package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
  34. package/templates/project/java/service/JwtService.java.ejs +81 -0
  35. package/templates/project/java/service/UserService.java.ejs +32 -0
  36. package/templates/project/resources/application.yml.ejs +50 -0
  37. package/templates/project/resources/logback-spring.xml.ejs +41 -0
  38. package/templates/project/static/admin.html.ejs +163 -0
  39. package/templates/project/static/assets/app.js.ejs +340 -0
  40. package/templates/project/static/assets/style.css +533 -0
  41. package/templates/project/static/css/style.css +595 -0
  42. package/templates/project/static/dashboard.html.ejs +119 -0
  43. package/templates/project/static/index.html.ejs +96 -0
  44. package/templates/project/static/js/api.js +30 -0
  45. package/templates/project/static/js/auth.js +44 -0
  46. package/templates/project/static/js/nav.js.ejs +82 -0
  47. package/templates/project/static/js/ui.js +57 -0
  48. package/templates/project/static/login.html.ejs +71 -0
  49. package/templates/project/static/profile.html.ejs +163 -0
  50. package/templates/project/static/register.html.ejs +82 -0
  51. package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
  52. package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
  53. package/templates/project/thymeleaf/layout.html.ejs +75 -0
  54. package/templates/project/thymeleaf/login.html.ejs +56 -0
  55. package/templates/project/thymeleaf/profile.html.ejs +133 -0
  56. package/templates/project/thymeleaf/register.html.ejs +56 -0
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # Sprygen
2
+
3
+ A fully-functional project generator tool similar to JHipster, written in Node.js with TypeScript, that can scaffold Spring Boot projects with authentication and common modules pre-configured.
4
+
5
+ ## Features
6
+
7
+ * **Generate Spring Boot Project**: Scaffolds a full Spring Boot 3.x project with Maven or Gradle.
8
+ * **Pre-configured Auth**: Includes JWT authentication via Spring Security.
9
+ * **Database Support**: Choose between H2 (in-memory), MySQL, or PostgreSQL.
10
+ * **Optional Modules**: Switch on Swagger OpenAPI, Spring Mail, and custom Logback logging.
11
+ * **Entity Generator**: Quickly add new JPA entities with full repository, service, and controller layers (CRUD REST API).
12
+ * **Standalone Auth Generator**: Add JWT security configurations to any existing Spring Boot project.
13
+
14
+ ## Installation
15
+
16
+ Install globally using npm:
17
+
18
+ ```bash
19
+ npm install -g sprygen
20
+ ```
21
+
22
+ *(For local development, clone the repository, run `npm install`, then `npm run build`, and `npm link`)*
23
+
24
+ ## Commands
25
+
26
+ ### `sprygen new <project-name>`
27
+
28
+ Launches an interactive prompt to scaffold a new project.
29
+
30
+ ```bash
31
+ sprygen new my-awesome-api
32
+ ```
33
+
34
+ It will prompt you for:
35
+ - Package name (e.g. `com.example.app`)
36
+ - Description
37
+ - Build Tool (Maven/Gradle)
38
+ - Database (H2/MySQL/PostgreSQL)
39
+ - Java Version (21/17)
40
+ - Optional Modules (Swagger, Mail, Logging)
41
+
42
+ ### `sprygen add-entity <entity-name>`
43
+
44
+ Must be run inside the generated project's root folder. It prompts you to define fields (types and nullability) for your entity and automatically generates:
45
+ - JPA Entity class
46
+ - Spring Data JpaRepository
47
+ - Service class
48
+ - REST Controller with standard CRUD endpoints
49
+ - DTO class
50
+ - Controller integration test stub
51
+
52
+ ```bash
53
+ cd my-awesome-api
54
+ sprygen add-entity Product
55
+ ```
56
+
57
+ ### `sprygen generate-auth`
58
+
59
+ Scaffolds JWT authentication and Spring Security configuration. Useful if you want to add Sprygen's security setup to an existing project not scaffolded by `sprygen new`.
60
+
61
+ ```bash
62
+ sprygen generate-auth
63
+ ```
64
+
65
+ ## Development
66
+
67
+ ```bash
68
+ # Install dependencies
69
+ npm install
70
+
71
+ # Run the CLI in development mode using ts-node
72
+ npm run dev new test-project
73
+
74
+ # Build the project (compiles TypeScript to dist/)
75
+ npm run build
76
+ ```
77
+
78
+ ## License
79
+
80
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env node
2
+ "use strict";var me=Object.create;var W=Object.defineProperty;var ge=Object.getOwnPropertyDescriptor;var de=Object.getOwnPropertyNames;var ue=Object.getPrototypeOf,je=Object.prototype.hasOwnProperty;var G=(t,e)=>()=>(t&&(e=t(t=0)),e);var fe=(t,e)=>{for(var a in e)W(t,a,{get:e[a],enumerable:!0})},he=(t,e,a,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let i of de(e))!je.call(t,i)&&i!==a&&W(t,i,{get:()=>e[i],enumerable:!(r=ge(e,i))||r.enumerable});return t};var j=(t,e,a)=>(a=t!=null?me(ue(t)):{},he(e||!t||!t.__esModule?W(a,"default",{value:t,enumerable:!0}):a,t));function M(t="1.0.0"){console.log(),ye.forEach((r,i)=>{i<2?process.stdout.write(d.default.hex(v.soft)(r)+`
3
+ `):process.stdout.write(d.default.hex(v.bright)(r)+`
4
+ `)}),console.log();let e=" Spring Boot Project Generator",a=`v${t}`;process.stdout.write(d.default.hex(v.base)(e)+d.default.dim(" \xB7 ")+d.default.dim(a)+`
5
+ `),console.log(d.default.hex(v.soft)(" "+"\u2500".repeat(62))),console.log()}function N(t){console.log(`
6
+ `+d.default.hex(v.bright)(" \u25C6 ")+d.default.bold.white(t))}function we(){console.log(d.default.hex(v.soft)(" "+"\u2500".repeat(62)))}var d,v,ye,p,x=G(()=>{"use strict";d=j(require("chalk")),v={bright:"#3fb950",base:"#2da44e",dim:"#26913d",soft:"#156e2e"},ye=[" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557"," \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551"," \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u255A\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551"," \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u255A\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551"," \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551"," \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D"];p={info:t=>console.log(` ${d.default.hex(v.base)("\xB7")} ${d.default.white(t)}`),success:t=>console.log(` ${d.default.hex(v.bright)("+")} ${d.default.white(t)}`),warn:t=>console.log(` ${d.default.yellow("!")} ${d.default.yellow(t)}`),error:t=>console.error(` ${d.default.red("\xD7")} ${d.default.red(t)}`),title:t=>{console.log(),console.log(d.default.hex(v.bright)(" \u25C6 ")+d.default.bold.white(t)),console.log()},file:t=>console.log(` ${d.default.hex(v.soft)("write")} ${d.default.dim(t)}`),step:(t,e,a)=>console.log(d.default.hex(v.soft)(` [${t}/${e}]`)+" "+d.default.white(a)),done:()=>{console.log(),we(),console.log(` ${d.default.hex(v.bright)("+")} ${d.default.bold.white("Done.")}`),console.log()}}});var E={};fe(E,{copyTemplate:()=>H,renderTemplate:()=>V,writeGeneratedFile:()=>y});async function V(t,e){let a=await P.default.readFile(t,"utf-8");return _.default.render(a,e,{async:!1})}async function y(t,e,a){let r=await V(e,a);await P.default.ensureDir(T.default.dirname(t)),await P.default.writeFile(t,r,"utf-8"),p.file(t)}async function H(t,e,a){let r=await P.default.readdir(t,{withFileTypes:!0});for(let i of r){let n=T.default.join(t,i.name),o=ve(i.name,a),c=o.endsWith(".ejs")?o.slice(0,-4):o,l=T.default.join(e,c);i.isDirectory()?(await P.default.ensureDir(l),await H(n,l,a)):i.name.endsWith(".ejs")?await y(l,n,a):(await P.default.copy(n,l),p.file(l))}}function ve(t,e){return t.replace(/\{\{(\w+)\}\}/g,(a,r)=>String(e[r]??`{{${r}}}`))}var _,P,T,D=G(()=>{"use strict";_=j(require("ejs")),P=j(require("fs-extra")),T=j(require("path"));x()});var pe=require("commander");var I=j(require("inquirer"));var s=j(require("path")),u=j(require("fs-extra")),Z=j(require("ora")),Y=j(require("axios")),Q=j(require("adm-zip"));x();var F=class{templatesDir;constructor(){let e=[s.default.resolve(__dirname,"../../templates/project"),s.default.resolve(__dirname,"../templates/project")],a=e.find(r=>u.default.existsSync(r));if(!a)throw new Error(`Could not locate templates directory. Tried:
7
+ ${e.join(`
8
+ `)}`);this.templatesDir=a}async generate(e){let a=s.default.resolve(process.cwd(),e.projectName);if(await u.default.pathExists(a))throw new Error(`Directory "${e.projectName}" already exists. Please choose a different project name or remove the existing directory.`);let r=e.projectName.toLowerCase().replace(/[^a-z0-9-]/g,"-"),i=e.packageName.split(".").slice(0,-1).join(".")||e.packageName;p.title(`Generating Spring Boot project: ${e.projectName}`);let n=(0,Z.default)({text:"Downloading from Spring Initializr\u2026",color:"cyan"}).start();try{let o="data-jpa,security,validation,lombok";o+=",web",e.database==="mysql"&&(o+=",mysql"),e.database==="postgresql"&&(o+=",postgresql"),e.database==="h2"&&(o+=",h2"),e.modules.includes("Mail")&&(o+=",mail");let c=e.authStrategy==="session"&&e.projectType==="fullstack";c&&(o+=",thymeleaf");let l=e.buildTool==="maven"?"maven-project":"gradle-project",m=new URL("https://start.spring.io/starter.zip");m.searchParams.append("type",l),m.searchParams.append("language","java"),m.searchParams.append("baseDir",e.projectName),m.searchParams.append("groupId",i),m.searchParams.append("artifactId",r),m.searchParams.append("name",e.projectName),m.searchParams.append("description",e.description),m.searchParams.append("packageName",e.packageName),m.searchParams.append("packaging","jar"),m.searchParams.append("javaVersion",e.javaVersion),m.searchParams.append("dependencies",o);let g=await(0,Y.default)({url:m.toString(),method:"GET",responseType:"arraybuffer"});n.text="Extracting project scaffold\u2026";let h=s.default.resolve(process.cwd(),`${e.projectName}-tmp.zip`);await u.default.writeFile(h,g.data),new Q.default(h).extractAllTo(process.cwd(),!0),await u.default.unlink(h),n.text="Patching dependencies\u2026",await this.patchDependencies(a,e,c),n.text="Writing source files\u2026";let $=this.buildContext(e,a,r,i),S=s.default.join(a,"src/main/java",e.packagePath),C=s.default.join(a,"src/main/resources");await u.default.ensureDir(S),await u.default.ensureDir(C);let B=s.default.join(C,"application.properties");await u.default.pathExists(B)&&await u.default.unlink(B),await this.generateJavaSources(S,$,e),await this.generateResources(C,$,e),e.projectType==="fullstack"&&(n.text="Writing frontend files\u2026",await this.generateFrontend(a,$,e,c)),n.succeed(`Project "${e.projectName}" scaffolded successfully!`)}catch(o){throw n.fail("Project generation failed."),o}this.printSuccessMessage(e)}async patchDependencies(e,a,r){let i=a.modules.includes("Swagger");if(a.buildTool==="maven"){let n=s.default.join(e,"pom.xml"),o=await u.default.readFile(n,"utf8"),c=a.authStrategy==="jwt"?`
9
+ <!-- JWT Dependencies -->
10
+ <dependency>
11
+ <groupId>io.jsonwebtoken</groupId>
12
+ <artifactId>jjwt-api</artifactId>
13
+ <version>0.11.5</version>
14
+ </dependency>
15
+ <dependency>
16
+ <groupId>io.jsonwebtoken</groupId>
17
+ <artifactId>jjwt-impl</artifactId>
18
+ <version>0.11.5</version>
19
+ <scope>runtime</scope>
20
+ </dependency>
21
+ <dependency>
22
+ <groupId>io.jsonwebtoken</groupId>
23
+ <artifactId>jjwt-jackson</artifactId>
24
+ <version>0.11.5</version>
25
+ <scope>runtime</scope>
26
+ </dependency>`:"",l=i?`
27
+ <!-- Swagger/OpenAPI -->
28
+ <dependency>
29
+ <groupId>org.springdoc</groupId>
30
+ <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
31
+ <version>2.3.0</version>
32
+ </dependency>`:"",m=r?`
33
+ <!-- Thymeleaf Spring Security Extras -->
34
+ <dependency>
35
+ <groupId>org.thymeleaf.extras</groupId>
36
+ <artifactId>thymeleaf-extras-springsecurity6</artifactId>
37
+ </dependency>`:"";o=o.replace("</dependencies>",`${c}${l}${m}
38
+ </dependencies>`),await u.default.writeFile(n,o,"utf8")}else{let n=s.default.join(e,"build.gradle"),o=i?`
39
+ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'`:"",c=r?`
40
+ implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'`:"",m=`
41
+ dependencies {${a.authStrategy==="jwt"?`
42
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
43
+ runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
44
+ runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'`:""}${o}${c}
45
+ }
46
+ `;await u.default.appendFile(n,m,"utf8")}}buildContext(e,a,r,i){let n=this.toPascalCase(e.projectName.replace(/[^a-zA-Z0-9]/g,""))+"Application",o=this.getDbDependencies(e.database),c=this.getDbConfig(e.database,r);return{projectName:e.projectName,projectNamePascal:n.replace("Application",""),artifactId:r,groupId:i,packageName:e.packageName,packagePath:e.packagePath,database:e.database,buildTool:e.buildTool,mainClassName:n,javaVersion:e.javaVersion,springBootVersion:e.springBootVersion,description:e.description,hasSwagger:e.modules.includes("Swagger"),hasMail:e.modules.includes("Mail"),hasLogging:e.modules.includes("Logging"),isJwtAuth:e.authStrategy==="jwt",isSessionAuth:e.authStrategy==="session",isFullstack:e.projectType==="fullstack",isApiOnly:e.projectType==="api",dbDependency:o.dependency,dbDriverClass:o.driverClass,dbConfig:c,outputDir:a,modules:e.modules,year:new Date().getFullYear()}}async generateJavaSources(e,a,r){let{writeGeneratedFile:i}=await Promise.resolve().then(()=>(D(),E)),n=s.default.join(this.templatesDir,"java"),o=a,c=[["entity/User.java","entity/User.java.ejs"],["entity/Role.java","entity/Role.java.ejs"],["repository/UserRepository.java","repository/UserRepository.java.ejs"],["service/UserService.java","service/UserService.java.ejs"],["controller/AuthController.java","controller/AuthController.java.ejs"],["controller/UserController.java","controller/UserController.java.ejs"],["controller/HomeController.java","controller/HomeController.java.ejs"],["controller/ProfileController.java","controller/ProfileController.java.ejs"],["controller/AdminController.java","controller/AdminController.java.ejs"],["config/CorsConfig.java","config/CorsConfig.java.ejs"],["dto/AuthRequest.java","dto/AuthRequest.java.ejs"],["dto/AuthResponse.java","dto/AuthResponse.java.ejs"],["dto/RegisterRequest.java","dto/RegisterRequest.java.ejs"],["dto/ProfileUpdateRequest.java","dto/ProfileUpdateRequest.java.ejs"],["dto/UserDto.java","dto/UserDto.java.ejs"]];r.authStrategy==="jwt"?c.push(["service/JwtService.java","service/JwtService.java.ejs"],["security/JwtAuthFilter.java","security/JwtAuthFilter.java.ejs"],["security/UserDetailsServiceImpl.java","security/UserDetailsServiceImpl.java.ejs"],["config/SecurityConfig.java","config/SecurityConfig.java.ejs"]):c.push(["security/UserDetailsServiceImpl.java","security/UserDetailsServiceImpl.java.ejs"],["config/SecurityConfig.java","config/SecurityConfigSession.java.ejs"]),r.modules.includes("Swagger")&&c.push(["config/SwaggerConfig.java","config/SwaggerConfig.java.ejs"]);for(let[l,m]of c){let g=s.default.join(n,m);await u.default.pathExists(g)?await i(s.default.join(e,l),g,o):p.warn(`Template not found, skipping: ${m}`)}}async generateResources(e,a,r){let{writeGeneratedFile:i}=await Promise.resolve().then(()=>(D(),E)),n=s.default.join(this.templatesDir,"resources"),o=a;await i(s.default.join(e,"application.yml"),s.default.join(n,"application.yml.ejs"),o),r.modules.includes("Logging")&&await i(s.default.join(e,"logback-spring.xml"),s.default.join(n,"logback-spring.xml.ejs"),o)}async generateFrontend(e,a,r,i){let{writeGeneratedFile:n}=await Promise.resolve().then(()=>(D(),E)),o=a;if(i){let c=s.default.join(this.templatesDir,"thymeleaf"),l=s.default.join(e,"src/main/resources/templates");await u.default.ensureDir(l),await u.default.ensureDir(s.default.join(l,"admin"));let m=[["login.html","login.html.ejs"],["register.html","register.html.ejs"],["dashboard.html","dashboard.html.ejs"],["profile.html","profile.html.ejs"],["admin/users.html","admin/users.html.ejs"]];for(let[h,b]of m)await n(s.default.join(l,h),s.default.join(c,b),o);let g=s.default.join(e,"src/main/resources/static/css");await u.default.ensureDir(g),await u.default.copyFile(s.default.join(this.templatesDir,"static/css/style.css"),s.default.join(g,"style.css"))}else{let c=s.default.join(this.templatesDir,"static"),l=s.default.join(e,"src/main/resources/static");await u.default.ensureDir(s.default.join(l,"css")),await u.default.ensureDir(s.default.join(l,"js"));let m=[["index.html","index.html.ejs"],["login.html","login.html.ejs"],["register.html","register.html.ejs"],["dashboard.html","dashboard.html.ejs"],["profile.html","profile.html.ejs"],["admin.html","admin.html.ejs"]];for(let[g,h]of m){let b=s.default.join(c,h);await u.default.pathExists(b)&&await n(s.default.join(l,g),b,o)}await n(s.default.join(l,"js/nav.js"),s.default.join(c,"js/nav.js.ejs"),o),await u.default.copyFile(s.default.join(c,"js/auth.js"),s.default.join(l,"js/auth.js")),await u.default.copyFile(s.default.join(c,"js/ui.js"),s.default.join(l,"js/ui.js")),await u.default.copyFile(s.default.join(c,"css/style.css"),s.default.join(l,"css/style.css"))}}toPascalCase(e){return e.charAt(0).toUpperCase()+e.slice(1)}getDbDependencies(e){switch(e){case"mysql":return{dependency:"mysql",driverClass:"com.mysql.cj.jdbc.Driver"};case"postgresql":return{dependency:"postgresql",driverClass:"org.postgresql.Driver"};default:return{dependency:"h2",driverClass:"org.h2.Driver"}}}getDbConfig(e,a){switch(e){case"mysql":return{url:`jdbc:mysql://localhost:3306/${a}?useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true`,username:"root",password:"YOUR_PASSWORD",dialect:"org.hibernate.dialect.MySQLDialect"};case"postgresql":return{url:`jdbc:postgresql://localhost:5432/${a}`,username:"postgres",password:"YOUR_PASSWORD",dialect:"org.hibernate.dialect.PostgreSQLDialect"};default:return{url:`jdbc:h2:mem:${a}`,username:"sa",password:"",dialect:"org.hibernate.dialect.H2Dialect"}}}printSuccessMessage(e){let a=e.authStrategy==="jwt",r=e.projectType==="fullstack",i=e.buildTool==="maven"?"./mvnw spring-boot:run":"./gradlew bootRun";console.log(),p.success(`Project "${e.projectName}" is ready!
47
+ `),console.log(` Next steps:
48
+ `),console.log(` cd ${e.projectName}`),console.log(` ${i}`),console.log(),console.log(" Endpoints:"),console.log(" Home http://localhost:8080/"),r&&console.log(` Dashboard http://localhost:8080/${a?"":"dashboard"}`),e.modules.includes("Swagger")&&console.log(" API Docs http://localhost:8080/swagger-ui/index.html"),console.log(" Health http://localhost:8080/actuator/health"),console.log(),console.log(` Auth strategy: ${a?"JWT (stateless)":"Session (form-login)"}`),console.log(` Project type: ${r?"Fullstack":"REST API only"}`),console.log()}};function K(t){return!t||t.trim()===""?"Package name cannot be empty.":/^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$/.test(t.trim())?!0:"Package name must be lowercase, dot-separated identifiers (e.g. com.example.myapp)."}function X(t){return!t||t.trim()===""?"Project name cannot be empty.":/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(t.trim())?!0:"Project name must start with a letter and contain only alphanumerics, hyphens, or underscores."}function ee(t){return!t||t.trim()===""?"Entity name cannot be empty.":/^[A-Za-z][A-Za-z0-9]*$/.test(t.trim())?!0:"Entity name must start with a letter and contain only alphanumerics (PascalCase recommended)."}function te(t){return t.replace(/\./g,"/")}function ae(t){return t.charAt(0).toUpperCase()+t.slice(1)}x();async function re(t){M("1.0.0");let e=X(t);typeof e=="string"&&(p.error(e),process.exit(1)),N("Configure Project");let a=await I.default.prompt([{type:"input",name:"packageName",message:"Package name:",default:`com.example.${t.toLowerCase().replace(/[^a-z0-9]/g,"")}`,validate:K},{type:"input",name:"description",message:"Project description:",default:`${t} Spring Boot Application`},{type:"list",name:"buildTool",message:"Build tool:",choices:[{name:"Maven (recommended)",value:"maven"},{name:"Gradle (Groovy DSL)",value:"gradle"}],default:"maven"},{type:"list",name:"database",message:"Database:",choices:[{name:"H2 (in-memory, great for dev/testing)",value:"h2"},{name:"MySQL ",value:"mysql"},{name:"PostgreSQL",value:"postgresql"}],default:"h2"},{type:"list",name:"javaVersion",message:"Java version:",choices:["21","17"],default:"21"}]);N("Authentication & Frontend");let r=await I.default.prompt([{type:"list",name:"authStrategy",message:"Authentication method:",choices:[{name:"JWT \u2014 stateless, token-based, ideal for REST + SPA (recommended)",value:"jwt"},{name:"Session \u2014 Spring form-login, stateful, ideal for Thymeleaf apps",value:"session"}],default:"jwt"},{type:"list",name:"projectType",message:"Project type:",choices:[{name:"REST API \u2014 JSON responses only, no frontend",value:"api"},{name:"Fullstack \u2014 REST API + frontend (dashboard, profiles, admin panel)",value:"fullstack"}],default:"api"}]);N("Select Modules");let i=await I.default.prompt([{type:"checkbox",name:"modules",message:"Optional modules (space to toggle):",choices:[{name:"Swagger / OpenAPI UI",value:"Swagger",checked:!0},{name:"Mail (Spring Mail SMTP)",value:"Mail"},{name:"Logging (Logback with file appender)",value:"Logging"}]}]),n={projectName:t,packageName:a.packageName,packagePath:te(a.packageName),database:a.database,buildTool:a.buildTool,modules:i.modules,javaVersion:a.javaVersion,springBootVersion:"3.2.4",description:a.description,authStrategy:r.authStrategy,projectType:r.projectType},o=new F;try{N(`Generating ${t}`),console.log(),await o.generate(n),p.done()}catch(c){console.log(),c instanceof Error?p.error(c.message):p.error("An unexpected error occurred."),process.exit(1)}}var O=j(require("inquirer")),U=j(require("fs-extra")),J=j(require("path"));var f=j(require("path")),z=j(require("fs-extra")),ie=j(require("ora"));D();x();var R=class{templatesDir;constructor(){this.templatesDir=f.default.resolve(__dirname,"../templates/entity")}async generate(e){p.title(`Generating entity: ${e.entityName}`);let a=(0,ie.default)({text:"Scaffolding entity files...",color:"cyan"}).start();try{let r=f.default.join(e.projectDir,"src/main/java",e.packagePath),i=f.default.join(e.projectDir,"src/test/java",e.packagePath);if(!await z.default.pathExists(r))throw new Error(`Cannot find Java source directory at "${r}". Make sure you are running this command inside a Sprygen-generated project.`);let n=this.buildContext(e);a.text="Writing entity, repository, service, controller...",await y(f.default.join(r,"entity",`${e.entityName}.java`),f.default.join(this.templatesDir,"Entity.java.ejs"),n),await y(f.default.join(r,"repository",`${e.entityName}Repository.java`),f.default.join(this.templatesDir,"EntityRepository.java.ejs"),n),await y(f.default.join(r,"service",`${e.entityName}Service.java`),f.default.join(this.templatesDir,"EntityService.java.ejs"),n),await y(f.default.join(r,"controller",`${e.entityName}Controller.java`),f.default.join(this.templatesDir,"EntityController.java.ejs"),n),await y(f.default.join(r,"dto",`${e.entityName}Dto.java`),f.default.join(this.templatesDir,"EntityDto.java.ejs"),n),a.text="Writing entity test...",await z.default.ensureDir(f.default.join(i,"controller")),await y(f.default.join(i,"controller",`${e.entityName}ControllerTest.java`),f.default.join(this.templatesDir,"EntityControllerTest.java.ejs"),n),a.succeed(`Entity "${e.entityName}" generated successfully!`)}catch(r){throw a.fail("Entity generation failed."),r}p.success(`
49
+ Entity "${e.entityName}" files created.
50
+ `)}buildContext(e){return{entityName:e.entityName,entityNameLower:e.entityNameLower,entityNameUpper:e.entityNameUpper,packageName:e.packageName,packagePath:e.packagePath,fields:e.fields,year:new Date().getFullYear()}}};x();async function oe(t){let e=ee(t);typeof e=="string"&&(p.error(e),process.exit(1));let a=process.cwd(),r="";try{let g=J.default.join(a,"src/main/java");if(await U.default.pathExists(g)){let h=await U.default.readdir(g);if(h.length>0){let b=J.default.join(g,h[0]);for(r=h[0];;){let S=(await U.default.readdir(b,{withFileTypes:!0})).filter(C=>C.isDirectory());if(S.length===1)r+="."+S[0].name,b=J.default.join(b,S[0].name);else break}}}}catch{}let i=await O.default.prompt([{type:"input",name:"packageName",message:"Base package name (e.g. com.example.myapp):",default:r||"com.example.myapp"}]),n=[],o=!0;for(p.info(`
51
+ Add fields to your entity (leave name empty to finish)
52
+ `);o;){let g=await O.default.prompt([{type:"input",name:"name",message:"Field name (camelCase):"}]);if(!g.name||g.name.trim()===""){o=!1;break}let h=await O.default.prompt([{type:"list",name:"type",message:`Type for ${g.name}:`,choices:["String","Integer","Long","Boolean","Double","LocalDate","LocalDateTime"],default:"String"},{type:"confirm",name:"nullable",message:"Can it be null?",default:!1}]);n.push({name:g.name,type:h.type,nullable:h.nullable})}let l={entityName:ae(t),entityNameLower:t.toLowerCase(),entityNameUpper:t.toUpperCase(),packageName:i.packageName,packagePath:i.packageName.replace(/\./g,"/"),fields:n,projectDir:a},m=new R;try{await m.generate(l)}catch(g){g instanceof Error?p.error(g.message):p.error("An unexpected error occurred."),process.exit(1)}}var ce=j(require("inquirer")),q=j(require("fs-extra")),A=j(require("path"));var w=j(require("path")),ne=j(require("fs-extra")),se=j(require("ora"));D();x();var L=class{templatesDir;constructor(){this.templatesDir=w.default.resolve(__dirname,"../templates/auth")}async generate(e){p.title("Generating JWT Authentication");let a=(0,se.default)({text:"Scaffolding security files...",color:"cyan"}).start();try{let r=w.default.join(e.projectDir,"src/main/java",e.packagePath);if(!await ne.default.pathExists(r))throw new Error("Cannot find Java source directory. Make sure you are inside a Sprygen project directory.");let i={packageName:e.packageName,packagePath:e.packagePath,projectName:e.projectName,year:new Date().getFullYear()};a.text="Writing SecurityConfig.java...",await y(w.default.join(r,"config","SecurityConfig.java"),w.default.join(this.templatesDir,"SecurityConfig.java.ejs"),i),a.text="Writing JwtService.java...",await y(w.default.join(r,"service","JwtService.java"),w.default.join(this.templatesDir,"JwtService.java.ejs"),i),a.text="Writing JwtAuthFilter.java...",await y(w.default.join(r,"security","JwtAuthFilter.java"),w.default.join(this.templatesDir,"JwtAuthFilter.java.ejs"),i),a.text="Writing AuthController.java...",await y(w.default.join(r,"controller","AuthController.java"),w.default.join(this.templatesDir,"AuthController.java.ejs"),i),a.text="Writing UserDetailsServiceImpl.java...",await y(w.default.join(r,"security","UserDetailsServiceImpl.java"),w.default.join(this.templatesDir,"UserDetailsServiceImpl.java.ejs"),i),a.succeed("JWT authentication scaffolded successfully!")}catch(r){throw a.fail("Auth generation failed."),r}p.success(`
53
+ JWT auth files generated.
54
+ `),p.info(`Make sure your application.yml has: jwt.secret and jwt.expiration set.
55
+ `)}};x();async function le(){let t=process.cwd(),e="",a=A.default.basename(t);try{let o=A.default.join(t,"src/main/java");if(await q.default.pathExists(o)){let c=await q.default.readdir(o);if(c.length>0){let l=A.default.join(o,c[0]);for(e=c[0];;){let g=(await q.default.readdir(l,{withFileTypes:!0})).filter(h=>h.isDirectory());if(g.length===1)e+="."+g[0].name,l=A.default.join(l,g[0].name);else break}}}}catch{}let r=await ce.default.prompt([{type:"input",name:"packageName",message:"Base package name (e.g. com.example.myapp):",default:e||"com.example.myapp"}]),i={packageName:r.packageName,packagePath:r.packageName.replace(/\./g,"/"),projectDir:t,projectName:a},n=new L;try{await n.generate(i)}catch(o){o instanceof Error?p.error(o.message):p.error("An unexpected error occurred."),process.exit(1)}}var k=new pe.Command;k.name("sprygen").description("A JHipster-like Spring Boot project generator CLI").version("1.0.0");k.command("new <project-name>").description("Generate a new Spring Boot project").action(async t=>{await re(t)});k.command("add-entity <entity-name>").description("Generate a repository, service, controller, and test for a new entity in an existing project").action(async t=>{await oe(t)});k.command("generate-auth").description("Scaffold JWT authentication and security configuration in an existing project").action(async()=>{await le()});k.parse(process.argv);process.argv.slice(2).length||k.outputHelp();
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "sprygen",
3
+ "version": "1.0.0",
4
+ "description": "A JHipster-like Spring Boot project generator CLI written in TypeScript",
5
+ "main": "dist/cli.js",
6
+ "bin": {
7
+ "sprygen": "./dist/cli.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsup src/cli.ts --format cjs --minify --out-dir dist",
15
+ "dev": "ts-node src/cli.ts",
16
+ "lint": "tsc --noEmit",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "adm-zip": "^0.5.17",
21
+ "axios": "^1.14.0",
22
+ "chalk": "4.1.2",
23
+ "commander": "^11.1.0",
24
+ "ejs": "^3.1.10",
25
+ "fs-extra": "^11.2.0",
26
+ "inquirer": "^8.2.6",
27
+ "ora": "5.4.1"
28
+ },
29
+ "devDependencies": {
30
+ "@types/adm-zip": "^0.5.8",
31
+ "@types/ejs": "^3.1.5",
32
+ "@types/fs-extra": "^11.0.4",
33
+ "@types/inquirer": "^8.2.10",
34
+ "@types/node": "^20.11.0",
35
+ "ts-node": "^10.9.2",
36
+ "tsup": "^8.0.2",
37
+ "typescript": "^5.3.3"
38
+ },
39
+ "keywords": [
40
+ "spring-boot",
41
+ "generator",
42
+ "scaffold",
43
+ "jhipster",
44
+ "java",
45
+ "jwt",
46
+ "cli"
47
+ ],
48
+ "author": "Sprygen",
49
+ "license": "MIT",
50
+ "engines": {
51
+ "node": ">=18.0.0"
52
+ }
53
+ }
@@ -0,0 +1,40 @@
1
+ package <%= packageName %>.controller;
2
+
3
+ import <%= packageName %>.service.JwtService;
4
+ import lombok.RequiredArgsConstructor;
5
+ import org.springframework.http.ResponseEntity;
6
+ import org.springframework.security.authentication.AuthenticationManager;
7
+ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
8
+ import org.springframework.security.core.userdetails.UserDetails;
9
+ import org.springframework.security.core.userdetails.UserDetailsService;
10
+ import org.springframework.web.bind.annotation.PostMapping;
11
+ import org.springframework.web.bind.annotation.RequestBody;
12
+ import org.springframework.web.bind.annotation.RequestMapping;
13
+ import org.springframework.web.bind.annotation.RestController;
14
+
15
+ import java.util.Map;
16
+
17
+ @RestController
18
+ @RequestMapping("/api/v1/auth")
19
+ @RequiredArgsConstructor
20
+ public class AuthController {
21
+
22
+ private final JwtService jwtService;
23
+ private final AuthenticationManager authenticationManager;
24
+ private final UserDetailsService userDetailsService;
25
+
26
+ @PostMapping("/login")
27
+ public ResponseEntity<?> login(@RequestBody Map<String, String> request) {
28
+ String email = request.get("email");
29
+ String password = request.get("password");
30
+
31
+ authenticationManager.authenticate(
32
+ new UsernamePasswordAuthenticationToken(email, password)
33
+ );
34
+
35
+ UserDetails user = userDetailsService.loadUserByUsername(email);
36
+ String jwtToken = jwtService.generateToken(user);
37
+
38
+ return ResponseEntity.ok(Map.of("token", jwtToken));
39
+ }
40
+ }
@@ -0,0 +1,62 @@
1
+ package <%= packageName %>.security;
2
+
3
+ import <%= packageName %>.service.JwtService;
4
+ import jakarta.servlet.FilterChain;
5
+ import jakarta.servlet.ServletException;
6
+ import jakarta.servlet.http.HttpServletRequest;
7
+ import jakarta.servlet.http.HttpServletResponse;
8
+ import lombok.RequiredArgsConstructor;
9
+ import org.springframework.lang.NonNull;
10
+ import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
11
+ import org.springframework.security.core.context.SecurityContextHolder;
12
+ import org.springframework.security.core.userdetails.UserDetails;
13
+ import org.springframework.security.core.userdetails.UserDetailsService;
14
+ import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
15
+ import org.springframework.stereotype.Component;
16
+ import org.springframework.web.filter.OncePerRequestFilter;
17
+
18
+ import java.io.IOException;
19
+
20
+ @Component
21
+ @RequiredArgsConstructor
22
+ public class JwtAuthFilter extends OncePerRequestFilter {
23
+
24
+ private final JwtService jwtService;
25
+ private final UserDetailsService userDetailsService;
26
+
27
+ @Override
28
+ protected void doFilterInternal(
29
+ @NonNull HttpServletRequest request,
30
+ @NonNull HttpServletResponse response,
31
+ @NonNull FilterChain filterChain
32
+ ) throws ServletException, IOException {
33
+ final String authHeader = request.getHeader("Authorization");
34
+ final String jwt;
35
+ final String userEmail;
36
+
37
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
38
+ filterChain.doFilter(request, response);
39
+ return;
40
+ }
41
+
42
+ jwt = authHeader.substring(7);
43
+ userEmail = jwtService.extractUsername(jwt);
44
+
45
+ if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
46
+ UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
47
+
48
+ if (jwtService.isTokenValid(jwt, userDetails)) {
49
+ UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
50
+ userDetails,
51
+ null,
52
+ userDetails.getAuthorities()
53
+ );
54
+ authToken.setDetails(
55
+ new WebAuthenticationDetailsSource().buildDetails(request)
56
+ );
57
+ SecurityContextHolder.getContext().setAuthentication(authToken);
58
+ }
59
+ }
60
+ filterChain.doFilter(request, response);
61
+ }
62
+ }
@@ -0,0 +1,81 @@
1
+ package <%= packageName %>.service;
2
+
3
+ import io.jsonwebtoken.Claims;
4
+ import io.jsonwebtoken.Jwts;
5
+ import io.jsonwebtoken.SignatureAlgorithm;
6
+ import io.jsonwebtoken.io.Decoders;
7
+ import io.jsonwebtoken.security.Keys;
8
+ import org.springframework.beans.factory.annotation.Value;
9
+ import org.springframework.security.core.userdetails.UserDetails;
10
+ import org.springframework.stereotype.Service;
11
+
12
+ import java.security.Key;
13
+ import java.util.Date;
14
+ import java.util.HashMap;
15
+ import java.util.Map;
16
+ import java.util.function.Function;
17
+
18
+ @Service
19
+ public class JwtService {
20
+
21
+ @Value("${jwt.secret:404E635266556A586E3272357538782F413F4428472B4B6250645367566B5970}")
22
+ private String secretKey;
23
+
24
+ @Value("${jwt.expiration:86400000}")
25
+ private long jwtExpiration;
26
+
27
+ public String extractUsername(String token) {
28
+ return extractClaim(token, Claims::getSubject);
29
+ }
30
+
31
+ public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
32
+ final Claims claims = extractAllClaims(token);
33
+ return claimsResolver.apply(claims);
34
+ }
35
+
36
+ public String generateToken(UserDetails userDetails) {
37
+ return generateToken(new HashMap<>(), userDetails);
38
+ }
39
+
40
+ public String generateToken(Map<String, Object> extraClaims, UserDetails userDetails) {
41
+ return buildToken(extraClaims, userDetails, jwtExpiration);
42
+ }
43
+
44
+ private String buildToken(Map<String, Object> extraClaims, UserDetails userDetails, long expiration) {
45
+ return Jwts
46
+ .builder()
47
+ .setClaims(extraClaims)
48
+ .setSubject(userDetails.getUsername())
49
+ .setIssuedAt(new Date(System.currentTimeMillis()))
50
+ .setExpiration(new Date(System.currentTimeMillis() + expiration))
51
+ .signWith(getSignInKey(), SignatureAlgorithm.HS256)
52
+ .compact();
53
+ }
54
+
55
+ public boolean isTokenValid(String token, UserDetails userDetails) {
56
+ final String username = extractUsername(token);
57
+ return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
58
+ }
59
+
60
+ private boolean isTokenExpired(String token) {
61
+ return extractExpiration(token).before(new Date());
62
+ }
63
+
64
+ private Date extractExpiration(String token) {
65
+ return extractClaim(token, Claims::getExpiration);
66
+ }
67
+
68
+ private Claims extractAllClaims(String token) {
69
+ return Jwts
70
+ .parserBuilder()
71
+ .setSigningKey(getSignInKey())
72
+ .build()
73
+ .parseClaimsJws(token)
74
+ .getBody();
75
+ }
76
+
77
+ private Key getSignInKey() {
78
+ byte[] keyBytes = Decoders.BASE64.decode(secretKey);
79
+ return Keys.hmacShaKeyFor(keyBytes);
80
+ }
81
+ }
@@ -0,0 +1,65 @@
1
+ package <%= packageName %>.config;
2
+
3
+ import <%= packageName %>.security.JwtAuthFilter;
4
+ import lombok.RequiredArgsConstructor;
5
+ import org.springframework.context.annotation.Bean;
6
+ import org.springframework.context.annotation.Configuration;
7
+ import org.springframework.security.authentication.AuthenticationManager;
8
+ import org.springframework.security.authentication.AuthenticationProvider;
9
+ import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
10
+ import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
11
+ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
12
+ import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
13
+ import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
14
+ import org.springframework.security.config.http.SessionCreationPolicy;
15
+ import org.springframework.security.core.userdetails.UserDetailsService;
16
+ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
17
+ import org.springframework.security.crypto.password.PasswordEncoder;
18
+ import org.springframework.security.web.SecurityFilterChain;
19
+ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
20
+
21
+ @Configuration
22
+ @EnableWebSecurity
23
+ @RequiredArgsConstructor
24
+ public class SecurityConfig {
25
+
26
+ private final JwtAuthFilter jwtAuthFilter;
27
+ private final UserDetailsService userDetailsService;
28
+
29
+ @Bean
30
+ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
31
+ http
32
+ .csrf(AbstractHttpConfigurer::disable)
33
+ .authorizeHttpRequests(authorize -> authorize
34
+ .requestMatchers("/api/v1/auth/**").permitAll()
35
+ .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
36
+ .requestMatchers("/actuator/**").permitAll()
37
+ .anyRequest().authenticated()
38
+ )
39
+ .sessionManagement(session -> session
40
+ .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
41
+ )
42
+ .authenticationProvider(authenticationProvider())
43
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
44
+
45
+ return http.build();
46
+ }
47
+
48
+ @Bean
49
+ public AuthenticationProvider authenticationProvider() {
50
+ // Spring Security 6.x: UserDetailsService must be passed to the constructor
51
+ DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(userDetailsService);
52
+ authProvider.setPasswordEncoder(passwordEncoder());
53
+ return authProvider;
54
+ }
55
+
56
+ @Bean
57
+ public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
58
+ return config.getAuthenticationManager();
59
+ }
60
+
61
+ @Bean
62
+ public PasswordEncoder passwordEncoder() {
63
+ return new BCryptPasswordEncoder();
64
+ }
65
+ }
@@ -0,0 +1,24 @@
1
+ package <%= packageName %>.security;
2
+
3
+ import lombok.RequiredArgsConstructor;
4
+ import org.springframework.security.core.userdetails.UserDetails;
5
+ import org.springframework.security.core.userdetails.UserDetailsService;
6
+ import org.springframework.security.core.userdetails.UsernameNotFoundException;
7
+ import org.springframework.stereotype.Service;
8
+
9
+ @Service
10
+ @RequiredArgsConstructor
11
+ public class UserDetailsServiceImpl implements UserDetailsService {
12
+
13
+ // Note: Inject your UserRepository here
14
+ // private final UserRepository userRepository;
15
+
16
+ @Override
17
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
18
+ // Implement database lookup here
19
+ // return userRepository.findByEmail(username)
20
+ // .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
21
+
22
+ throw new UsernameNotFoundException("User lookup not implemented yet. Please inject your UserRepository.");
23
+ }
24
+ }
@@ -0,0 +1,40 @@
1
+ package <%= packageName %>.entity;
2
+
3
+ import jakarta.persistence.*;
4
+ import lombok.AllArgsConstructor;
5
+ import lombok.Builder;
6
+ import lombok.Data;
7
+ import lombok.NoArgsConstructor;
8
+
9
+ <%_
10
+ const hasLocalDate = fields.some(f => f.type === 'LocalDate');
11
+ const hasLocalDateTime = fields.some(f => f.type === 'LocalDateTime');
12
+
13
+ if (hasLocalDate) { _%>
14
+ import java.time.LocalDate;
15
+ <%_ }
16
+ if (hasLocalDateTime) { _%>
17
+ import java.time.LocalDateTime;
18
+ <%_ } _%>
19
+
20
+ @Data
21
+ @Builder
22
+ @NoArgsConstructor
23
+ @AllArgsConstructor
24
+ @Entity
25
+ @Table(name = "<%= entityNameLower %>s")
26
+ public class <%= entityName %> {
27
+
28
+ @Id
29
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
30
+ private Long id;
31
+
32
+ <%_ for (let i = 0; i < fields.length; i++) {
33
+ let field = fields[i]; _%>
34
+ <%_ if (!field.nullable) { _%>
35
+ @Column(nullable = false)
36
+ <%_ } _%>
37
+ private <%= field.type %> <%= field.name %>;
38
+
39
+ <%_ } _%>
40
+ }
@@ -0,0 +1,92 @@
1
+ package <%= packageName %>.controller;
2
+
3
+ import <%= packageName %>.dto.<%= entityName %>Dto;
4
+ import <%= packageName %>.entity.<%= entityName %>;
5
+ import <%= packageName %>.service.<%= entityName %>Service;
6
+ import lombok.RequiredArgsConstructor;
7
+ import org.springframework.http.HttpStatus;
8
+ import org.springframework.http.ResponseEntity;
9
+ import org.springframework.web.bind.annotation.*;
10
+
11
+ import jakarta.validation.Valid;
12
+ import java.util.List;
13
+ import java.util.stream.Collectors;
14
+
15
+ @RestController
16
+ @RequestMapping("/api/v1/<%= entityNameLower %>s")
17
+ @RequiredArgsConstructor
18
+ public class <%= entityName %>Controller {
19
+
20
+ private final <%= entityName %>Service service;
21
+
22
+ @GetMapping
23
+ public ResponseEntity<List<<%= entityName %>Dto>> getAll() {
24
+ List<<%= entityName %>Dto> list = service.findAll().stream()
25
+ .map(this::mapToDto)
26
+ .collect(Collectors.toList());
27
+ return ResponseEntity.ok(list);
28
+ }
29
+
30
+ @GetMapping("/{id}")
31
+ public ResponseEntity<<%= entityName %>Dto> getById(@PathVariable Long id) {
32
+ return service.findById(id)
33
+ .map(this::mapToDto)
34
+ .map(ResponseEntity::ok)
35
+ .orElse(ResponseEntity.notFound().build());
36
+ }
37
+
38
+ @PostMapping
39
+ public ResponseEntity<<%= entityName %>Dto> create(@Valid @RequestBody <%= entityName %>Dto dto) {
40
+ <%= entityName %> entity = mapToEntity(dto);
41
+ <%= entityName %> saved = service.save(entity);
42
+ return ResponseEntity.status(HttpStatus.CREATED).body(mapToDto(saved));
43
+ }
44
+
45
+ @PutMapping("/{id}")
46
+ public ResponseEntity<<%= entityName %>Dto> update(@PathVariable Long id, @Valid @RequestBody <%= entityName %>Dto dto) {
47
+ return service.findById(id)
48
+ .map(existing -> {
49
+ // Update fields here
50
+ <%_ for (let i = 0; i < fields.length; i++) {
51
+ let field = fields[i];
52
+ let pascalName = field.name.charAt(0).toUpperCase() + field.name.slice(1); _%>
53
+ existing.set<%= pascalName %>(dto.get<%= pascalName %>());
54
+ <%_ } _%>
55
+ return service.save(existing);
56
+ })
57
+ .map(this::mapToDto)
58
+ .map(ResponseEntity::ok)
59
+ .orElse(ResponseEntity.notFound().build());
60
+ }
61
+
62
+ @DeleteMapping("/{id}")
63
+ public ResponseEntity<Void> delete(@PathVariable Long id) {
64
+ if (service.findById(id).isEmpty()) {
65
+ return ResponseEntity.notFound().build();
66
+ }
67
+ service.deleteById(id);
68
+ return ResponseEntity.noContent().build();
69
+ }
70
+
71
+ private <%= entityName %>Dto mapToDto(<%= entityName %> entity) {
72
+ return <%= entityName %>Dto.builder()
73
+ .id(entity.getId())
74
+ <%_ for (let i = 0; i < fields.length; i++) {
75
+ let field = fields[i];
76
+ let pascalName = field.name.charAt(0).toUpperCase() + field.name.slice(1); _%>
77
+ .<%= field.name %>(entity.get<%= pascalName %>())
78
+ <%_ } _%>
79
+ .build();
80
+ }
81
+
82
+ private <%= entityName %> mapToEntity(<%= entityName %>Dto dto) {
83
+ return <%= entityName %>.builder()
84
+ .id(dto.getId())
85
+ <%_ for (let i = 0; i < fields.length; i++) {
86
+ let field = fields[i];
87
+ let pascalName = field.name.charAt(0).toUpperCase() + field.name.slice(1); _%>
88
+ .<%= field.name %>(dto.get<%= pascalName %>())
89
+ <%_ } _%>
90
+ .build();
91
+ }
92
+ }