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.
- package/README.md +80 -0
- package/dist/cli.js +55 -0
- package/package.json +53 -0
- package/templates/auth/AuthController.java.ejs +40 -0
- package/templates/auth/JwtAuthFilter.java.ejs +62 -0
- package/templates/auth/JwtService.java.ejs +81 -0
- package/templates/auth/SecurityConfig.java.ejs +65 -0
- package/templates/auth/UserDetailsServiceImpl.java.ejs +24 -0
- package/templates/entity/Entity.java.ejs +40 -0
- package/templates/entity/EntityController.java.ejs +92 -0
- package/templates/entity/EntityControllerTest.java.ejs +24 -0
- package/templates/entity/EntityDto.java.ejs +32 -0
- package/templates/entity/EntityRepository.java.ejs +9 -0
- package/templates/entity/EntityService.java.ejs +32 -0
- package/templates/project/java/config/CorsConfig.java.ejs +24 -0
- package/templates/project/java/config/SecurityConfig.java.ejs +76 -0
- package/templates/project/java/config/SecurityConfigSession.java.ejs +73 -0
- package/templates/project/java/config/SwaggerConfig.java.ejs +31 -0
- package/templates/project/java/controller/AdminController.java.ejs +82 -0
- package/templates/project/java/controller/AuthController.java.ejs +86 -0
- package/templates/project/java/controller/HomeController.java.ejs +63 -0
- package/templates/project/java/controller/ProfileController.java.ejs +65 -0
- package/templates/project/java/controller/UserController.java.ejs +35 -0
- package/templates/project/java/dto/AuthRequest.java.ejs +15 -0
- package/templates/project/java/dto/AuthResponse.java.ejs +18 -0
- package/templates/project/java/dto/ProfileUpdateRequest.java.ejs +20 -0
- package/templates/project/java/dto/RegisterRequest.java.ejs +30 -0
- package/templates/project/java/dto/UserDto.java.ejs +17 -0
- package/templates/project/java/entity/Role.java.ejs +6 -0
- package/templates/project/java/entity/User.java.ejs +97 -0
- package/templates/project/java/repository/UserRepository.java.ejs +11 -0
- package/templates/project/java/security/JwtAuthFilter.java.ejs +62 -0
- package/templates/project/java/security/UserDetailsServiceImpl.java.ejs +21 -0
- package/templates/project/java/service/JwtService.java.ejs +81 -0
- package/templates/project/java/service/UserService.java.ejs +32 -0
- package/templates/project/resources/application.yml.ejs +50 -0
- package/templates/project/resources/logback-spring.xml.ejs +41 -0
- package/templates/project/static/admin.html.ejs +163 -0
- package/templates/project/static/assets/app.js.ejs +340 -0
- package/templates/project/static/assets/style.css +533 -0
- package/templates/project/static/css/style.css +595 -0
- package/templates/project/static/dashboard.html.ejs +119 -0
- package/templates/project/static/index.html.ejs +96 -0
- package/templates/project/static/js/api.js +30 -0
- package/templates/project/static/js/auth.js +44 -0
- package/templates/project/static/js/nav.js.ejs +82 -0
- package/templates/project/static/js/ui.js +57 -0
- package/templates/project/static/login.html.ejs +71 -0
- package/templates/project/static/profile.html.ejs +163 -0
- package/templates/project/static/register.html.ejs +82 -0
- package/templates/project/thymeleaf/admin/users.html.ejs +111 -0
- package/templates/project/thymeleaf/dashboard.html.ejs +109 -0
- package/templates/project/thymeleaf/layout.html.ejs +75 -0
- package/templates/project/thymeleaf/login.html.ejs +56 -0
- package/templates/project/thymeleaf/profile.html.ejs +133 -0
- 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
|
+
}
|