diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java new file mode 100644 index 00000000000..a0db2e0178a --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationService.java @@ -0,0 +1,19 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtService; +import tech.jhipster.lite.generator.project.domain.Project; + +@Service +public class VueJwtApplicationService { + + private final VueJwtService vueJwtService; + + public VueJwtApplicationService(VueJwtService jwtService) { + this.vueJwtService = jwtService; + } + + public void addJWT(Project project) { + vueJwtService.addJWT(project); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java new file mode 100644 index 00000000000..cc162bf6a64 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwt.java @@ -0,0 +1,286 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static tech.jhipster.lite.common.domain.FileUtils.getPath; +import static tech.jhipster.lite.generator.project.domain.Constants.PACKAGE_JSON; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import tech.jhipster.lite.common.domain.FileUtils; +import tech.jhipster.lite.generator.project.domain.Project; + +public class VueJwt { + + private VueJwt() {} + + public static final Collection MAIN_PROVIDES = List.of( + "const authenticationRepository = new AuthenticationRepository(axiosHttp, pinia);", + "app.provide('authenticationService', authenticationRepository);", + "app.provide('logger', consoleLogger);", + "app.provide('router', router);" + ); + public static final Collection MAIN_PROVIDER = List.of( + "const axiosHttp = new AxiosHttp(axios.create({ baseURL: '' }));", + "const consoleLogger = new ConsoleLogger(console);" + ); + public static final Collection MAIN_IMPORTS = List.of( + "import AuthenticationRepository from './common/secondary/AuthenticationRepository';", + "import { AxiosHttp } from './http/AxiosHttp';", + "import axios from 'axios';", + "import ConsoleLogger from './common/secondary/ConsoleLogger';", + "import Homepage from './common/primary/homepage/Homepage.vue';" + ); + + public static boolean isPiniaNotImplemented(Project project) { + return !FileUtils.containsLines(getPath(project.getFolder(), PACKAGE_JSON), List.of("\"pinia\":")); + } + + public static List primaryLoginFiles() { + return List.of("index.ts", "Login.component.ts", "Login.html", "Login.vue"); + } + + public static final Collection LOGIN_ROUTES = List.of( + " {", + " path: '/login',", + " name: 'Login',", + " component: LoginVue,", + " },", + " {", + " path: '/',", + " name: 'Homepage',", + " component: AppVue,", + " }," + ); + + public static final Collection ROUTER_IMPORTS = List.of("import { LoginVue } from '@/common/primary/login';"); + + public static Collection primaryHomepageFiles() { + return List.of("index.ts", "Homepage.component.ts", "Homepage.html", "Homepage.vue"); + } + + public static Collection domainFiles() { + return List.of("AuthenticationService.ts", "JWTStoreService.ts", "Login.ts", "User.ts"); + } + + public static Collection secondaryFiles() { + return List.of("AuthenticationRepository.ts", "UserDTO.ts"); + } + + public static Collection testDomainFiles() { + return List.of("AuthenticationService.fixture.ts", "JWTStoreService.spec.ts"); + } + + public static Collection testSecondaryFiles() { + return List.of("AuthenticationRepository.spec.ts", "RestLogin.spec.ts", "UserDTO.spec.ts"); + } + + public static Map appComponent() { + return Map.of( + """ + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { Logger } from '@/common/domain/Logger'; + import { User } from '@/common/domain/User'; + import { Router } from 'vue-router'; + import { jwtStore } from '@/common/domain/JWTStoreService'; + """, + "import", + """ + const authenticationService = inject('authenticationService') as AuthenticationService; + const logger = inject('logger') as Logger; + const router = inject('router') as Router; + + let store = jwtStore(); + let isAuthenticated:boolean = store.isAuth; + let user = ref({ + username: '', + authorities: [''], + }); + + const onConnect = async (): Promise => { + await authenticationService + .authenticate() + .then(response => { + user.value = response; + }) + .catch(error => { + logger.error('The token provided is not know by our service', error); + }); + } + + const onLogout = async (): Promise => { + authenticationService + .logout(); + router.push("/login"); + }; + """, + "setup", + """ + user, + isAuthenticated, + onConnect, + onLogout, + """, + "return" + ); + } + + public static Map appTest() { + return Map.of( + """ + import { createTestingPinia } from '@pinia/testing'; + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { stubAuthenticationService } from '../../domain/AuthenticationService.fixture'; + import { stubLogger } from '../../domain/Logger.fixture'; + import { Logger } from '@/common/domain/Logger'; + import sinon from 'sinon'; + """, + "test-import", + """ + const \\$route = { path: {} }; + const router = { push: sinon.stub() }; + """, + "test-variables", + """ + authenticationService: AuthenticationService; + logger: Logger; + """, + "test-wrapper-options", + """ + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + """, + "test-wrapper-variable", + """ + global: { + stubs: ['router-link'], + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia({ + initialState: { + JWTStore: {token: '123456789'}, + }, + })], + }, + """, + "test-wrapper-mount", + """ + it('should authenticate', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.resolves({ username: 'username', authorities: ['admin'] }); + await wrap({ authenticationService, logger }); + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + + // @ts-ignore + expect(wrapper.vm.user).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); + + it('Should log an error when authentication fails', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.rejects({}); + await wrap({ authenticationService, logger }); + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + + const [message] = logger.error.getCall(0).args; + expect(message).toBe('The token provided is not know by our service'); + }); + + it('Should log out', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.authenticate.resolves({ username: 'username', authorities: ['admin'] }); + await wrap({ authenticationService, logger }); + + + const clickButton = wrapper.find('#identify'); + await clickButton.trigger('click'); + const logoutButton = wrapper.find('#logout'); + await logoutButton.trigger('click'); + + sinon.assert.calledOnce(authenticationService.logout); + }); + """, + "test-routes" + ); + } + + public static List appHTML() { + return List.of( + "
", + "
", + "

You are connected as

", + "
", + " ", + "
", + "
", + "

{{user.username}}

", + " ", + "
", + "
", + "
", + "

You are not connected

", + " Login", + "
", + "
" + ); + } + + public static Map routerspec() { + return Map.of( + """ + import { LoginVue } from '@/common/primary/login'; + import { createTestingPinia } from '@pinia/testing'; + import { AuthenticationService } from '@/common/domain/AuthenticationService'; + import { stubAuthenticationService } from '../common/domain/AuthenticationService.fixture'; + import { stubLogger } from '../common/domain/Logger.fixture'; + import { Logger } from '@/common/domain/Logger'; + """, + "test-import", + """ + authenticationService: AuthenticationService; + logger: Logger; + """, + "test-wrapper-options", + """ + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + """, + "test-wrapper-variable", + """ + global: { + stubs: ['router-link'], + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia()], + }, + """, + "test-wrapper-mount", + """ + it('Should go to LoginVue', async () => { + router.push('/Login'); + await wrapper.vm.\\$nextTick(); + expect(wrapper.findComponent(LoginVue)).toBeTruthy(); + }); + afterAll(async () => new Promise(resolve => window.setTimeout(resolve, 0))); + """, + "test-routes" + ); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java new file mode 100644 index 00000000000..bbdccffb7d8 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainService.java @@ -0,0 +1,257 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static tech.jhipster.lite.common.domain.FileUtils.getPath; +import static tech.jhipster.lite.common.domain.WordUtils.LF; +import static tech.jhipster.lite.generator.project.domain.Constants.*; + +import java.io.IOException; +import java.util.List; +import tech.jhipster.lite.common.domain.FileUtils; +import tech.jhipster.lite.error.domain.Assert; +import tech.jhipster.lite.error.domain.GeneratorException; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.domain.ProjectFile; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +public class VueJwtDomainService implements VueJwtService { + + public static final String SOURCE = "client/vue"; + + public static final String SOURCE_JWT = "webapp/app/jwt/"; + public static final String SOURCE_TEST = "test/spec/jwt/"; + + public static final String DESTINATION_TEST = TEST_JAVASCRIPT + "/common/"; + + public static final String COMMON = "/app/common/"; + public static final String DESTINATION_PRIMARY = MAIN_WEBAPP + COMMON + PRIMARY; + public static final String SOURCE_PRIMARY = SOURCE_JWT + PRIMARY; + + public static final String DESTINATION_DOMAIN = MAIN_WEBAPP + COMMON + DOMAIN; + public static final String SOURCE_DOMAIN = SOURCE_JWT + DOMAIN; + + public static final String DESTINATION_SECONDARY = MAIN_WEBAPP + COMMON + SECONDARY; + public static final String SOURCE_SECONDARY = SOURCE_JWT + SECONDARY; + + public static final String NEEDLE_MAIN_IMPORT = "// jhipster-needle-main-ts-import"; + + public static final String NEEDLE_MAIN_PROVIDER = "// jhipster-needle-main-ts-provider"; + private static final String NEEDLE_MAIN_INSTANCIATION = "// jhipster-needle-main-ts-instanciation"; + + public static final String NEEDLE_ROUTER = "// jhipster-needle-router"; + + public static final String NEEDLE_APP = "// jhipster-needle-app"; + + public static final String LOGIN = "/login"; + + public static final String HOMEPAGE = "/homepage"; + + private final ProjectRepository projectRepository; + + public VueJwtDomainService(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + @Override + public void addJWT(Project project) { + checkIfProjectNotNull(project); + if (VueJwt.isPiniaNotImplemented(project)) { + throw new GeneratorException("Pinia has not been added"); + } + addAppContext(project); + addLoginContext(project); + addHomepageContext(project); + addDomainRelated(project); + addRoutes(project); + addMain(project); + addTests(project); + addSecondary(project); + } + + private void checkIfProjectNotNull(Project project) { + Assert.notNull("project", project); + } + + public void addAppContext(Project project) { + String destinationAppHtml = DESTINATION_PRIMARY + "/app/App.html"; + String destinationAppComponent = DESTINATION_PRIMARY + "/app"; + try { + FileUtils.appendLines(getPath(project.getFolder(), destinationAppHtml), VueJwt.appHTML()); + } catch (IOException e) { + throw new GeneratorException("Error when writing to app.html. Make sur this file exist", e); + } + VueJwt + .appComponent() + .forEach((line, needle) -> + addNewNeedleLineToFile(project, line, destinationAppComponent, "App.component.ts", NEEDLE_APP + "-" + needle) + ); + projectRepository.replaceText( + project, + destinationAppComponent, + "App.component.ts", + "import \\{ defineComponent \\} from \"vue\";", + "import { defineComponent, inject, ref } from \"vue\";" + ); + } + + public void addLoginContext(Project project) { + String destinationPrimaryLoginContext = DESTINATION_PRIMARY + LOGIN; + String sourcePrimaryLoginContext = SOURCE_PRIMARY + LOGIN; + projectRepository.template( + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_DOMAIN), "Login.ts").withDestinationFolder(DESTINATION_DOMAIN) + ); + List primaryFiles = VueJwt + .primaryLoginFiles() + .stream() + .map(entry -> + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, sourcePrimaryLoginContext), entry) + .withDestinationFolder(destinationPrimaryLoginContext) + ) + .toList(); + projectRepository.template(primaryFiles); + projectRepository.template( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, SOURCE_SECONDARY), "RestLogin.ts") + .withDestinationFolder(DESTINATION_SECONDARY) + ); + } + + public void addHomepageContext(Project project) { + String destinationPrimaryHomepageContext = DESTINATION_PRIMARY + HOMEPAGE; + String sourcePrimaryHomepageContext = SOURCE_PRIMARY + HOMEPAGE; + + List primaryHomepageFiles = VueJwt + .primaryHomepageFiles() + .stream() + .map(entry -> + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, sourcePrimaryHomepageContext), entry) + .withDestinationFolder(destinationPrimaryHomepageContext) + ) + .toList(); + + projectRepository.template(primaryHomepageFiles); + + projectRepository.add( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, "webapp/content/images"), "JHipster-Lite-neon-green.png") + .withDestinationFolder("src/main/webapp/content/images") + ); + projectRepository.add( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, "webapp/content/images"), "VueLogo.png") + .withDestinationFolder("src/main/webapp/content/images") + ); + } + + public void addRoutes(Project project) { + String routerPath = "src/main/webapp/app/router"; + + projectRepository.replaceText( + project, + getPath(routerPath), + ROUTER_TYPESCRIPT, + "redirect: \\{ name: 'App' \\}", + "redirect: { name: 'Homepage' }" + ); + VueJwt.ROUTER_IMPORTS.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, routerPath, ROUTER_TYPESCRIPT, NEEDLE_ROUTER + "-imports") + ); + VueJwt.LOGIN_ROUTES.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, routerPath, ROUTER_TYPESCRIPT, NEEDLE_ROUTER + "-routes") + ); + } + + public void addMain(Project project) { + String appPath = "src/main/webapp/app"; + projectRepository.replaceText(project, getPath(appPath), MAIN_TYPESCRIPT, "createApp\\(App\\)", "createApp(Homepage)"); + VueJwt.MAIN_IMPORTS.forEach(providerLine -> addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_IMPORT) + ); + VueJwt.MAIN_PROVIDER.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_INSTANCIATION) + ); + VueJwt.MAIN_PROVIDES.forEach(providerLine -> + addNewNeedleLineToFile(project, providerLine, appPath, MAIN_TYPESCRIPT, NEEDLE_MAIN_PROVIDER) + ); + } + + public void addTests(Project project) { + String testDomainPath = DESTINATION_TEST + "domain"; + String testPrimaryPath = DESTINATION_TEST + "primary"; + String testSecondaryPath = DESTINATION_TEST + "secondary"; + + List testDomainFiles = VueJwt + .testDomainFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_TEST + DOMAIN), entry).withDestinationFolder(testDomainPath) + ) + .toList(); + projectRepository.template(testDomainFiles); + VueJwt + .appTest() + .forEach((line, needle) -> + addNewNeedleLineToFile(project, line, "src/test/javascript/spec/common/primary/app", "App.spec.ts", NEEDLE_APP + "-" + needle) + ); + projectRepository.template( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, SOURCE_TEST + PRIMARY + LOGIN), "Login.spec.ts") + .withDestinationFolder(testPrimaryPath + LOGIN) + ); + projectRepository.template( + ProjectFile + .forProject(project) + .withSource(getPath(SOURCE, SOURCE_TEST + PRIMARY + HOMEPAGE), "Homepage.spec.ts") + .withDestinationFolder(testPrimaryPath + HOMEPAGE) + ); + + List testSecondaryFiles = VueJwt + .testSecondaryFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_TEST + SECONDARY), entry).withDestinationFolder(testSecondaryPath) + ) + .toList(); + + projectRepository.template(testSecondaryFiles); + + VueJwt + .routerspec() + .forEach((line, needle) -> + addNewNeedleLineToFile(project, line, getPath("src/test/javascript/spec/router"), "Router.spec.ts", NEEDLE_ROUTER + "-" + needle) + ); + } + + private void addNewNeedleLineToFile(Project project, String importLine, String folder, String file, String needle) { + String importWithNeedle = importLine + LF + needle; + projectRepository.replaceText(project, getPath(folder), file, needle, importWithNeedle); + } + + private void addSecondary(Project project) { + List secondaryFiles = VueJwt + .secondaryFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_SECONDARY), entry).withDestinationFolder(DESTINATION_SECONDARY) + ) + .toList(); + projectRepository.template(secondaryFiles); + } + + public void addDomainRelated(Project project) { + List domainFiles = VueJwt + .domainFiles() + .stream() + .map(entry -> + ProjectFile.forProject(project).withSource(getPath(SOURCE, SOURCE_DOMAIN), entry).withDestinationFolder(DESTINATION_DOMAIN) + ) + .toList(); + projectRepository.template(domainFiles); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java new file mode 100644 index 00000000000..dc86ae5c392 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtService.java @@ -0,0 +1,7 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import tech.jhipster.lite.generator.project.domain.Project; + +public interface VueJwtService { + void addJWT(Project project); +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java new file mode 100644 index 00000000000..38d5dd72ebb --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfiguration.java @@ -0,0 +1,22 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtDomainService; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtService; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +@Configuration +public class VueJwtBeanConfiguration { + + private final ProjectRepository projectRepository; + + public VueJwtBeanConfiguration(ProjectRepository projectRepository) { + this.projectRepository = projectRepository; + } + + @Bean + public VueJwtService vueJwtService() { + return new VueJwtDomainService(projectRepository); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java new file mode 100644 index 00000000000..1a9e23202b5 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/rest/VueJwtResource.java @@ -0,0 +1,35 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.primary.rest; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.jhipster.lite.generator.client.vue.security.jwt.application.VueJwtApplicationService; +import tech.jhipster.lite.generator.project.domain.GeneratorAction; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO; +import tech.jhipster.lite.technical.infrastructure.primary.annotation.GeneratorStep; + +@RestController +@RequestMapping("/api/clients/vue") +@Tag(name = "Vue") +class VueJwtResource { + + private final VueJwtApplicationService vueJwtApplicationService; + + public VueJwtResource(VueJwtApplicationService vueJwtApplicationService) { + this.vueJwtApplicationService = vueJwtApplicationService; + } + + @Operation(summary = "Add JWT to vue ap", description = "Add Jwt") + @ApiResponse(responseCode = "500", description = "An error occurred while adding JWT on Vue") + @PostMapping("/jwt") + @GeneratorStep(id = GeneratorAction.VUE_JWT) + public void addVueJwt(@RequestBody ProjectDTO projectDTO) { + Project project = ProjectDTO.toProject(projectDTO); + vueJwtApplicationService.addJWT(project); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java new file mode 100644 index 00000000000..99059af3542 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/client/vue/security/jwt/package-info.java @@ -0,0 +1,2 @@ +@tech.jhipster.lite.BusinessContext +package tech.jhipster.lite.generator.client.vue.security.jwt; diff --git a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java index 2831708eaaa..0fdc73c6125 100644 --- a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java +++ b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java @@ -125,6 +125,7 @@ private GeneratorAction() {} public static final String VUE = "vue"; public static final String VUE_PINIA = "vue-pinia"; + public static final String VUE_JWT = "vue-jwt"; public static final String JIB = "jib"; public static final String DOCKERFILE = "dockerfile"; diff --git a/src/main/resources/generator/client/vue/test/spec/common/primary/app/App.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/common/primary/app/App.spec.ts.mustache index 804ddf61459..ed3591e2a3c 100644 --- a/src/main/resources/generator/client/vue/test/spec/common/primary/app/App.spec.ts.mustache +++ b/src/main/resources/generator/client/vue/test/spec/common/primary/app/App.spec.ts.mustache @@ -1,10 +1,20 @@ import { shallowMount, VueWrapper } from '@vue/test-utils'; import { AppVue } from '@/common/primary/app'; +// jhipster-needle-app-test-import let wrapper: VueWrapper; +// jhipster-needle-app-test-variables -const wrap = () => { - wrapper = shallowMount(AppVue); +interface WrapperOptions { + // jhipster-needle-app-test-wrapper-options +} + +const wrap = (wrapperOptions?: Partial) => { + // jhipster-needle-app-test-wrapper-variable + + wrapper = shallowMount(AppVue,{ + // jhipster-needle-app-test-wrapper-mount + }); }; describe('App', () => { @@ -13,4 +23,6 @@ describe('App', () => { expect(wrapper.exists()).toBeTruthy(); }); + + // jhipster-needle-app-test-routes }); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache new file mode 100644 index 00000000000..18f6cb6b1ee --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/domain/AuthenticationService.fixture.ts.mustache @@ -0,0 +1,14 @@ +import sinon, { SinonSpy,SinonStub } from 'sinon'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; + +export interface AuthenticationServiceFixture extends AuthenticationService { + login: SinonStub; + authenticate: SinonStub; + logout: SinonSpy; +} + +export const stubAuthenticationService = (): AuthenticationServiceFixture => ({ + login: sinon.stub(), + authenticate: sinon.stub(), + logout: sinon.spy(), +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/domain/JWTStoreService.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/domain/JWTStoreService.spec.ts.mustache new file mode 100644 index 00000000000..ff35bf6abb0 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/domain/JWTStoreService.spec.ts.mustache @@ -0,0 +1,29 @@ +import { setActivePinia, createPinia, StoreDefinition } from 'pinia'; +import { jwtStore } from '@/common/domain/JWTStoreService'; + +describe('Test JWT store', () => { + const TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + let store: any; + + beforeEach(() => { + setActivePinia(createPinia()); + store = jwtStore(); + }); + + it('Should set token in store', () => { + store.setToken(TOKEN); + expect(store.token).toEqual(TOKEN); + }); + + it('Should remove token from store', () => { + store.setToken(TOKEN); + store.removeToken(); + expect(store.isAuth).toBeFalsy(); + }); + + it('Should tell user is loged in', () => { + store.setToken(TOKEN); + expect(store.isAuth).toBe(true); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/primary/homepage/Homepage.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/primary/homepage/Homepage.spec.ts.mustache new file mode 100644 index 00000000000..68268a513cd --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/primary/homepage/Homepage.spec.ts.mustache @@ -0,0 +1,20 @@ +import { shallowMount, VueWrapper } from '@vue/test-utils'; +import { HomepageVue } from '@/common/primary/homepage'; + +let wrapper: VueWrapper; + +const wrap = () => { + wrapper = shallowMount(HomepageVue, { + global: { + stubs: ['router-link', 'router-view'] + } + }); +}; + +describe('App', () => { + it('should exist', () => { + wrap(); + + expect(wrapper.exists()).toBeTruthy(); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/primary/login/Login.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/primary/login/Login.spec.ts.mustache new file mode 100644 index 00000000000..6fe139bb069 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/primary/login/Login.spec.ts.mustache @@ -0,0 +1,87 @@ +import { shallowMount, VueWrapper } from '@vue/test-utils'; +import { LoginVue } from '@/common/primary/login'; +import { createTestingPinia } from '@pinia/testing'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { stubAuthenticationService } from '../../domain/AuthenticationService.fixture'; +import { stubLogger } from '../../domain/Logger.fixture'; +import { Logger } from '@/common/domain/Logger'; +import { Login } from '@/common/domain/Login'; +import sinon from 'sinon'; + +let wrapper: VueWrapper; +const $route = { path: {} }; +const router = { push: sinon.stub() }; + +interface WrapperOptions { + authenticationService: AuthenticationService; + logger: Logger; +} + +const wrap = (wrapperOptions?: Partial) => { + const { authenticationService, logger }: WrapperOptions = { + authenticationService: stubAuthenticationService(), + logger: stubLogger(), + ...wrapperOptions, + }; + + wrapper = shallowMount(LoginVue, { + global: { + provide: { + authenticationService, + logger, + router, + }, + plugins: [createTestingPinia()], + }, + }); +}; + +const fillFullForm = async (login: Login): Promise => { + const usernameInput = wrapper.find('#username'); + await usernameInput.setValue(login.username); + const passwordInput = wrapper.find('#password'); + await passwordInput.setValue(login.password); +}; + +describe('Login', () => { + it('should exist', () => { + wrap(); + + expect(wrapper.exists()).toBe(true); + }); + + it('should login', async () => { + const authenticationService = stubAuthenticationService(); + authenticationService.login.resolves({}); + await wrap({ authenticationService }); + + const login: Login = { username: 'admin', password: 'admin', rememberMe: true }; + await fillFullForm(login); + + const submitButton = wrapper.find('#submit'); + await submitButton.trigger('submit'); + + const args = authenticationService.login.getCall(0).args[0]; + + expect(args).toEqual({ username: 'admin', password: 'admin', rememberMe: false }); + + // @ts-ignore + expect(wrapper.vm.getError()).toBeFalsy(); + }); + + it('Should log an error when login fails', async () => { + const authenticationService = stubAuthenticationService(); + const logger = stubLogger(); + authenticationService.login.rejects({}); + await wrap({ authenticationService, logger }); + + const login: Login = { username: 'admin', password: 'wrong_password', rememberMe: true }; + await fillFullForm(login); + + const submitButton = wrapper.find('#submit'); + await submitButton.trigger('submit'); + + const [message] = logger.error.getCall(0).args; + expect(message).toBe('Wrong credentials have been provided'); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache new file mode 100644 index 00000000000..adec68b20ff --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/AuthenticationRepository.spec.ts.mustache @@ -0,0 +1,78 @@ +import { Login } from '@/common/domain/Login'; +import { RestLogin } from '@/common/secondary/RestLogin'; +import { User } from '@/common/domain/User'; +import AuthenticationRepository from '@/common/secondary/AuthenticationRepository'; +import { AxiosHttpStub, stubAxiosHttp } from '../../http/AxiosHttpStub'; +import { createPinia, Pinia, setActivePinia, Store } from 'pinia'; +import { jwtStore } from '../../../../../main/webapp/app/common/domain/JWTStoreService'; + +let axiosHttpStub: AxiosHttpStub; +let piniaInstance: Pinia; +let store: Store; + +describe('AuthenticationRepository', () => { + const AUTH_TOKEN = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c'; + beforeEach(() => { + axiosHttpStub = stubAxiosHttp(); + piniaInstance = createPinia(); + setActivePinia(piniaInstance); + store = jwtStore(piniaInstance); + }); + it('Should login when status 200 returned', async () => { + axiosHttpStub.post.resolves({ + status: 200, + headers: { + authorization: 'Bearer ' + AUTH_TOKEN, + }, + }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + const login: Login = { username: 'admin', password: 'admin', rememberMe: true }; + + await authenticationRepository.login(login); + + const [uri, payload] = axiosHttpStub.post.getCall(0).args; + expect(uri).toBe('/api/authenticate'); + expect(payload).toEqual({ username: 'admin', password: 'admin', rememberMe: true }); + // @ts-ignore + expect(store.token).toEqual(AUTH_TOKEN); + }); + + it('Should set empty token', async () => { + axiosHttpStub.post.resolves({ status: 401, headers: { authorization: '' } }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + const login: Login = { username: 'admin', password: 'wrong_password', rememberMe: true }; + + await authenticationRepository.login(login); + + const [uri, payload] = axiosHttpStub.post.getCall(0).args; + expect(uri).toBe('/api/authenticate'); + expect(payload).toEqual({ username: 'admin', password: 'wrong_password', rememberMe: true }); + // @ts-ignore + expect(store.token).toEqual(''); + }); + + it('Should authenticate', async () => { + // @ts-ignore + store.setToken('fake_token'); + axiosHttpStub.get.resolves({ data: { login: 'username', authorities: ['admin'] } }); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + + const response = await authenticationRepository.authenticate(); + + const [uri, payload] = axiosHttpStub.get.getCall(0).args; + expect(uri).toBe('/api/account'); + expect(payload.headers.Authorization).toEqual('Bearer fake_token'); + expect(response).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); + + it('Should log out', async () => { + // @ts-ignore + store.setToken('fake_token'); + const authenticationRepository = new AuthenticationRepository(axiosHttpStub, piniaInstance); + + authenticationRepository.logout(); + // @ts-ignore + expect(store.token).toEqual(''); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache new file mode 100644 index 00000000000..e84d60751bd --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/RestLogin.spec.ts.mustache @@ -0,0 +1,10 @@ +import { RestLogin, toRestLogin } from '@/common/secondary/RestLogin'; +import { Login } from '@/common/domain/Login'; + +describe('RestLogin', () => { + it('should convert to RestLogin', () => { + const login: Login = { username: 'username', password: 'password', rememberMe: true }; + + expect(toRestLogin(login)).toEqual({ username: 'username', password: 'password', rememberMe: true }); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache new file mode 100644 index 00000000000..fa6a92e7420 --- /dev/null +++ b/src/main/resources/generator/client/vue/test/spec/jwt/secondary/UserDTO.spec.ts.mustache @@ -0,0 +1,10 @@ +import { UserDTO, toUser } from '@/common/secondary/UserDTO'; +import { User } from '@/common/domain/User'; + +describe('estLogin', () => { + it('should convert to User', () => { + const userDTO: UserDTO = { login: 'username', authorities: ['admin'] }; + + expect(toUser(userDTO)).toStrictEqual({ username: 'username', authorities: ['admin'] }); + }); +}); diff --git a/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache b/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache index f966f944f7a..46ef2b2d503 100644 --- a/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache +++ b/src/main/resources/generator/client/vue/test/spec/router/Router.spec.ts.mustache @@ -1,16 +1,25 @@ import { shallowMount, VueWrapper } from '@vue/test-utils'; import { AppVue } from '@/common/primary/app'; +// jhipster-needle-router-test-imports import router from '@/router/router'; let wrapper: VueWrapper; -const wrap = () => { - wrapper = shallowMount(AppVue,{ - router - }); +interface WrapperOptions { + // jhipster-needle-router-test-wrapper-options +} + +const wrap = (wrapperOptions?: Partial) => { + // jhipster-needle-router-test-wrapper-variable + + wrapper = shallowMount(AppVue,{ + // jhipster-needle-router-test-wrapper-mount + router + }); }; + describe('Router', () => { it('Should redirect to App by default', async () => { wrap(); @@ -27,4 +36,7 @@ describe('Router', () => { expect(wrapper.findComponent(AppVue)).toBeTruthy() }) + + // jhipster-needle-router-test-routes + }) diff --git a/src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.component.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.component.ts.mustache index 2b41fa35108..8da1041e51f 100644 --- a/src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.component.ts.mustache +++ b/src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.component.ts.mustache @@ -1,8 +1,16 @@ -export default { +import { defineComponent } from "vue"; +// jhipster-needle-app-import + +export default defineComponent({ name: 'App', - data: () => { + components: {}, + setup() { + const appName='Jhipster-lite'; + // jhipster-needle-app-setup + return { - appName: '{{baseName}}', + appName, + // jhipster-needle-app-return }; }, -}; +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache new file mode 100644 index 00000000000..265f06925cd --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/AuthenticationService.ts.mustache @@ -0,0 +1,8 @@ +import { Login } from '@/common/domain/Login'; +import { User } from '@/common/domain/User'; + +export interface AuthenticationService { + authenticate(): Promise; + login(login: Login): Promise; + logout(): void; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/JWTStoreService.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/JWTStoreService.ts.mustache new file mode 100644 index 00000000000..d55d25f1157 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/JWTStoreService.ts.mustache @@ -0,0 +1,31 @@ +import { defineStore } from 'pinia'; + +export const jwtStore = defineStore({ + id: 'JWTStore', + state: () => ({ + token: '', + }), + + getters: { + isAuth(state) { + return state.token != ''; + }, + }, + actions: { + setToken(token: string) { + this.token = token; + }, + removeToken(){ + this.token=''; + } + }, + persist: { + enabled: true, + strategies: [ + { + key: 'user', + storage: localStorage, + }, + ], + }, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache new file mode 100644 index 00000000000..bcd6e7cb0ce --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/Login.ts.mustache @@ -0,0 +1,5 @@ +export interface Login { + username: string; + password: string; + rememberMe: boolean; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache new file mode 100644 index 00000000000..3c753fd7e17 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/domain/User.ts.mustache @@ -0,0 +1,4 @@ +export interface User { + username: string; + authorities: string[]; +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.component.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.component.ts.mustache new file mode 100644 index 00000000000..92ed4e62ac8 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.component.ts.mustache @@ -0,0 +1,6 @@ +import { defineComponent} from 'vue'; + +export default defineComponent({ + name: 'Homepage', + components: {}, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.html.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.html.mustache new file mode 100644 index 00000000000..94d2629d6ee --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.html.mustache @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.vue.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.vue.mustache new file mode 100644 index 00000000000..999c90e77a0 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/Homepage.vue.mustache @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/index.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/index.ts.mustache new file mode 100644 index 00000000000..fce67b47eba --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/homepage/index.ts.mustache @@ -0,0 +1,4 @@ +import HomepageComponent from './Homepage.component'; +import HomepageVue from './Homepage.vue'; + +export { HomepageComponent, HomepageVue }; diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.component.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.component.ts.mustache new file mode 100644 index 00000000000..dcd11df96ab --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.component.ts.mustache @@ -0,0 +1,45 @@ +import { defineComponent, inject, ref } from 'vue'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { Logger } from '@/common/domain/Logger'; +import { Login } from '@/common/domain/Login'; +import { Router } from 'vue-router'; + +export default defineComponent({ + name: 'Login', + components: {}, + setup() { + const authenticationService = inject('authenticationService') as AuthenticationService; + const logger = inject('logger') as Logger; + const router = inject('router') as Router; + + const form = ref({ + username: '', + password: '', + rememberMe: false, + }); + + let loginError = false; + + const onSubmit = async (): Promise => { + await authenticationService + .login(form.value) + .then(() => { + router.push('/'); + }) + .catch(error => { + loginError = true; + logger.error('Wrong credentials have been provided', error); + }); + }; + + const getError = (): boolean => { + return loginError; + }; + + return { + onSubmit, + form, + getError, + }; + }, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.html.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.html.mustache new file mode 100644 index 00000000000..8f07768269d --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.html.mustache @@ -0,0 +1,22 @@ +
+
+

Login Form

+
+ +
+ + +
+ +
+ +
+
diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.vue.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.vue.mustache new file mode 100644 index 00000000000..034b84f1ca8 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/Login.vue.mustache @@ -0,0 +1,229 @@ + + + + + diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/index.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/index.ts.mustache new file mode 100644 index 00000000000..87d78427d59 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/primary/login/index.ts.mustache @@ -0,0 +1,4 @@ +import LoginComponent from './Login.component'; +import LoginVue from './Login.vue'; + +export { LoginComponent, LoginVue }; diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache new file mode 100644 index 00000000000..dd2b6ec0af5 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/AuthenticationRepository.ts.mustache @@ -0,0 +1,45 @@ +import { Login } from '@/common/domain/Login'; +import { RestLogin, toRestLogin } from '@/common/secondary/RestLogin'; + +import { AxiosHttp } from '@/http/AxiosHttp'; +import { AuthenticationService } from '@/common/domain/AuthenticationService'; +import { User } from '../domain/User'; +import { toUser, UserDTO } from './UserDTO'; +import { Pinia } from 'pinia'; +import { jwtStore } from '@/common/domain/JWTStoreService'; + +export default class AuthenticationRepository implements AuthenticationService { + constructor(private axiosHttp: AxiosHttp, private piniaInstance: Pinia) {} + + async authenticate(): Promise { + return this.axiosHttp + .get('/api/account', { headers: { Authorization: 'Bearer ' + this.getJwtToken() } }) + .then(response => toUser(response.data)); + } + + async login(login: Login): Promise { + const restLogin: RestLogin = toRestLogin(login); + await this.axiosHttp + .post('/api/authenticate', restLogin) + .then(response => this.saveJwtTokenIntoStore(this.parseAuthorisationHeaders(response))); + } + + logout(): void { + this.removeToken(); + } + + private saveJwtTokenIntoStore = (token: string): void => jwtStore(this.piniaInstance).setToken(token); + + private getJwtToken = (): string => jwtStore(this.piniaInstance).token; + + private removeToken = (): void => jwtStore(this.piniaInstance).removeToken(); + + parseAuthorisationHeaders(response: any): string { + const bearerToken = response.headers.authorization; + if (bearerToken && bearerToken.slice(0, 7) === 'Bearer ') { + return bearerToken.slice(7, bearerToken.length); + } else { + return ''; + } + } +} diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache new file mode 100644 index 00000000000..bef342d5a99 --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/RestLogin.ts.mustache @@ -0,0 +1,13 @@ +import { Login } from '@/common/domain/Login'; + +export interface RestLogin { + username: string; + password: string; + rememberMe: boolean; +} + +export const toRestLogin = (login: Login): RestLogin => ({ + username: login.username, + password: login.password, + rememberMe: login.rememberMe, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache new file mode 100644 index 00000000000..e596daf658b --- /dev/null +++ b/src/main/resources/generator/client/vue/webapp/app/jwt/secondary/UserDTO.ts.mustache @@ -0,0 +1,11 @@ +import { User } from '@/common/domain/User'; + +export interface UserDTO { + login: string; + authorities: string[]; +} + +export const toUser = (userDTO: UserDTO): User => ({ + username: userDTO.login, + authorities: userDTO.authorities, +}); diff --git a/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache index 0f5ea875b13..31a31d26a4a 100644 --- a/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache +++ b/src/main/resources/generator/client/vue/webapp/app/main.ts.mustache @@ -2,6 +2,8 @@ import { createApp } from 'vue'; import App from './common/primary/app/App.vue'; // jhipster-needle-main-ts-import +// jhipster-needle-main-ts-instanciation + const app = createApp(App); // jhipster-needle-main-ts-provider app.mount('#app'); diff --git a/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache b/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache index 26fed265ab0..a2843d577f9 100644 --- a/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache +++ b/src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache @@ -1,5 +1,7 @@ import { AppVue } from '@/common/primary/app'; import { createRouter, createWebHistory } from 'vue-router'; +// jhipster-needle-router-imports + const routes = [ { @@ -11,6 +13,7 @@ const routes = [ name: 'App', component: AppVue, }, + // jhipster-needle-router-routes ]; const router = createRouter({ diff --git a/src/test/features/vue.feature b/src/test/features/vue.feature new file mode 100644 index 00000000000..0e357877205 --- /dev/null +++ b/src/test/features/vue.feature @@ -0,0 +1,46 @@ +Feature: Vue + + Scenario: Should initialize Vue application + When I generate vue application + Then I should have files in "src/main/webapp" + | index.html | + And I should have files in "src/main/webapp/app" + | env.d.ts | + | main.ts | + And I should have files in "src/main/webapp/app/router" + | router.ts | + And I should have files in "src/main/webapp/app/http" + | AxiosHttp.ts | + And I should have files in "src/main/webapp/app/common/domain" + | Logger.ts | + | Message.ts | + And I should have files in "src/main/webapp/app/common/secondary" + | ConsoleLogger.ts | + And I should have files in "src/main/webapp/app/common/primary/app" + | App.component.ts | + | App.html | + | App.vue | + | index.ts | + + Scenario: Should add jwt authentication to Vue application + When I add jwt to vue application + Then I should have files in "src/main/webapp/app/common/domain" + | AuthenticationService.ts | + | JWTStoreService.ts | + | Login.ts | + | User.ts | + And I should have files in "src/main/webapp/app/common/primary/homepage" + | Homepage.component.ts | + | Homepage.html | + | Homepage.vue | + | index.ts | + And I should have files in "src/main/webapp/app/common/primary/login" + | Login.component.ts | + | Login.html | + | Login.vue | + | index.ts | + And I should have files in "src/main/webapp/app/common/secondary" + | AuthenticationRepository.ts | + | ConsoleLogger.ts | + | RestLogin.ts | + | UserDTO.ts | diff --git a/src/test/java/tech/jhipster/lite/TestUtils.java b/src/test/java/tech/jhipster/lite/TestUtils.java index 4d4784f57a3..fabc6bacdc1 100644 --- a/src/test/java/tech/jhipster/lite/TestUtils.java +++ b/src/test/java/tech/jhipster/lite/TestUtils.java @@ -137,6 +137,19 @@ public static Project tmpProjectWithPackageJsonComplete() { return project; } + public static Project tmpProjectWithPackageJsonPinia() { + Project project = tmpProject(); + copyPackageJsonByName(project, "package-pinia.json"); + return project; + } + + public static Project tmpProjectWithPiniaAndAppContext() { + Project project = tmpProject(); + copyPackageJsonByName(project, "package-pinia.json"); + copyVueAppFiles(project); + return project; + } + public static Project tmpProjectWithBuildGradle() { Project project = tmpProject(); copyBuildGradle(project); @@ -259,6 +272,27 @@ public static void copyLiquibaseMasterXml(Project project) { } } + private static void copyVueAppFiles(Project project) { + try { + FileUtils.createFolder(getPath(project.getFolder(), "src/main/webapp/app/common/primary/app")); + FileUtils.createFolder(getPath(project.getFolder(), "src/main/webapp/app/router")); + Files.copy( + getPathOf("src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.html.mustache"), + getPathOf(project.getFolder(), "src/main/webapp/app/common/primary/app", "App.html") + ); + Files.copy( + getPathOf("src/main/resources/generator/client/vue/webapp/app/common/primary/app/App.component.ts.mustache"), + getPathOf(project.getFolder(), "src/main/webapp/app/common/primary/app", "App.component.ts") + ); + Files.copy( + getPathOf("src/main/resources/generator/client/vue/webapp/app/router/router.ts.mustache"), + getPathOf(project.getFolder(), "src/main/webapp/app/router", "router.ts") + ); + } catch (IOException e) { + throw new AssertionError(e); + } + } + private static final ObjectMapper mapper = createObjectMapper(); private static ObjectMapper createObjectMapper() { diff --git a/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java b/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java index 9085dd08b82..fd0044c4ceb 100644 --- a/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java +++ b/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java @@ -21,10 +21,12 @@ public abstract class ModulesSteps { @Autowired private TestRestTemplate rest; - protected void applyModuleForDefaultProject(String moduleUrl) { + protected void applyModuleForDefaultProject(String... moduleUrl) { ProjectDTO project = newDefaultProjectDto(); - post(moduleUrl, JsonHelper.writeAsString(project)); + for (String url : moduleUrl) { + post(url, JsonHelper.writeAsString(project)); + } } protected void applyModuleForDefaultProjectWithMavenFile(String moduleUrl) { @@ -51,6 +53,22 @@ private static void addPomToproject(String folder) { } } + private static void addPackageToproject(String folder) { + Path folderPath = Paths.get(folder); + try { + Files.createDirectories(folderPath); + } catch (IOException e) { + throw new AssertionError(e); + } + + Path pomPath = folderPath.resolve("pom.xml"); + try { + Files.copy(Paths.get("src/test/resources/projects/maven/pom.xml"), pomPath); + } catch (IOException e) { + throw new AssertionError(e); + } + } + private void post(String uri, String content) { rest.exchange(uri, HttpMethod.POST, new HttpEntity<>(content, jsonHeaders()), Void.class); } diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueSteps.java b/src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueSteps.java new file mode 100644 index 00000000000..3579b810b51 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueSteps.java @@ -0,0 +1,17 @@ +package tech.jhipster.lite.generator.client.vue.core.infrastructure.primary.rest; + +import io.cucumber.java.en.When; +import tech.jhipster.lite.generator.ModulesSteps; + +public class VueSteps extends ModulesSteps { + + @When("I generate vue application") + public void addVue() { + applyModuleForDefaultProject("/api/inits/full", "/api/clients/vue"); + } + + @When("I add jwt to vue application") + public void addVueJwt() { + applyModuleForDefaultProject("/api/inits/full", "/api/clients/vue", "/api/clients/vue/stores/pinia", "/api/clients/vue/jwt"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java new file mode 100644 index 00000000000..dd76775a940 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtApplicationServiceIT.java @@ -0,0 +1,37 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.application; + +import static tech.jhipster.lite.TestUtils.*; +import static tech.jhipster.lite.generator.project.domain.Constants.MAIN_WEBAPP; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tech.jhipster.lite.IntegrationTest; +import tech.jhipster.lite.generator.client.vue.core.application.VueApplicationService; +import tech.jhipster.lite.generator.project.domain.Project; + +@IntegrationTest +class VueJwtApplicationServiceIT { + + @Autowired + VueJwtApplicationService vueJwtApplicationService; + + @Autowired + VueApplicationService vueApplicationService; + + @Test + void shouldAddVueJwt() { + Project project = tmpProjectWithPackageJson(); + vueApplicationService.addVue(project); + vueApplicationService.addPinia(project); + vueJwtApplicationService.addJWT(project); + String COMMON = MAIN_WEBAPP + "/app/common"; + assertFileExist(project, COMMON + "/domain/Login.ts"); + assertFileExist(project, COMMON + "/primary/login/index.ts"); + assertFileExist(project, COMMON + "/primary/login/Login.component.ts"); + assertFileExist(project, COMMON + "/primary/login/Login.html"); + assertFileExist(project, COMMON + "/primary/login/Login.vue"); + assertFileExist(project, COMMON + "/secondary/RestLogin.ts"); + assertFileExist(project, COMMON + "/domain/AuthenticationService.ts"); + assertFileExist(project, COMMON + "/domain/JWTStoreService.ts"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtAssert.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtAssert.java new file mode 100644 index 00000000000..1e027e4fc1b --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/application/VueJwtAssert.java @@ -0,0 +1,3 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.application; + +public class VueJwtAssert {} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java new file mode 100644 index 00000000000..73a98f239b2 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/domain/VueJwtDomainServiceTest.java @@ -0,0 +1,63 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.domain; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static tech.jhipster.lite.TestUtils.tmpProjectWithPackageJson; +import static tech.jhipster.lite.TestUtils.tmpProjectWithPackageJsonPinia; +import static tech.jhipster.lite.TestUtils.tmpProjectWithPiniaAndAppContext; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import tech.jhipster.lite.UnitTest; +import tech.jhipster.lite.error.domain.GeneratorException; +import tech.jhipster.lite.error.domain.MissingMandatoryValueException; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.domain.ProjectFile; +import tech.jhipster.lite.generator.project.domain.ProjectRepository; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class VueJwtDomainServiceTest { + + @Mock + ProjectRepository projectRepository; + + @InjectMocks + private VueJwtDomainService jwtDomainService; + + @Test + void shouldAddVueJwt() { + Project project = tmpProjectWithPiniaAndAppContext(); + + assertThatCode(() -> jwtDomainService.addJWT(project)).doesNotThrowAnyException(); + + verify(projectRepository, times(4)).template(any(ProjectFile.class)); + } + + @Test + void shouldNotWriteToAppContext() { + Project project = tmpProjectWithPackageJsonPinia(); + + assertThatThrownBy(() -> jwtDomainService.addJWT(project)).isExactlyInstanceOf(GeneratorException.class); + } + + @Test + void shouldNotAddVueJwt() { + Project project = tmpProjectWithPackageJson(); + + assertThatThrownBy(() -> jwtDomainService.addJWT(project)).isExactlyInstanceOf(GeneratorException.class); + } + + @Test + void shouldNotaddVueJwtWhenNoProject() { + assertThatThrownBy(() -> jwtDomainService.addJWT(null)) + .isExactlyInstanceOf(MissingMandatoryValueException.class) + .hasMessageContaining("project"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java new file mode 100644 index 00000000000..7983c96e299 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/config/VueJwtBeanConfigurationIT.java @@ -0,0 +1,21 @@ +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.config; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import tech.jhipster.lite.IntegrationTest; +import tech.jhipster.lite.generator.client.vue.security.jwt.domain.VueJwtDomainService; + +@IntegrationTest +class VueJwtBeanConfigurationIT { + + @Autowired + ApplicationContext applicationContext; + + @Test + void shouldGetBean() { + assertThat(applicationContext.getBean("vueJwtService")).isNotNull().isInstanceOf(VueJwtDomainService.class); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueResourceIT.java b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java similarity index 63% rename from src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueResourceIT.java rename to src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java index e5952bb3e60..b0dac324e20 100644 --- a/src/test/java/tech/jhipster/lite/generator/client/vue/core/infrastructure/primary/rest/VueResourceIT.java +++ b/src/test/java/tech/jhipster/lite/generator/client/vue/security/jwt/infrastructure/primary/VueJwtResourceIT.java @@ -1,4 +1,4 @@ -package tech.jhipster.lite.generator.client.vue.core.infrastructure.primary.rest; +package tech.jhipster.lite.generator.client.vue.security.jwt.infrastructure.primary; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -12,14 +12,13 @@ import org.springframework.test.web.servlet.MockMvc; import tech.jhipster.lite.IntegrationTest; import tech.jhipster.lite.TestUtils; -import tech.jhipster.lite.generator.client.vue.core.application.VueAssert; import tech.jhipster.lite.generator.init.application.InitApplicationService; import tech.jhipster.lite.generator.project.domain.Project; import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO; @IntegrationTest @AutoConfigureMockMvc -class VueResourceIT { +class VueJwtResourceIT { @Autowired MockMvc mockMvc; @@ -28,7 +27,7 @@ class VueResourceIT { InitApplicationService initApplicationService; @Test - void shouldAddVue() throws Exception { + void shouldAddVueJwt() throws Exception { ProjectDTO projectDTO = readFileToObject("json/chips.json", ProjectDTO.class).folder(tmpDirForTest()); Project project = ProjectDTO.toProject(projectDTO); initApplicationService.init(project); @@ -37,27 +36,13 @@ void shouldAddVue() throws Exception { .perform(post("/api/clients/vue").contentType(MediaType.APPLICATION_JSON).content(TestUtils.convertObjectToJsonBytes(projectDTO))) .andExpect(status().isOk()); - VueAssert.assertDependency(project); - VueAssert.assertScripts(project); - - VueAssert.assertViteConfigFiles(project); - VueAssert.assertRootFiles(project); - VueAssert.assertAppFiles(project); - VueAssert.assertAppWithCss(project); - - VueAssert.assertJestSonar(project); - } - - @Test - void shouldAddPinia() throws Exception { - ProjectDTO projectDTO = readFileToObject("json/chips.json", ProjectDTO.class).folder(tmpDirForTest()); - Project project = ProjectDTO.toProject(projectDTO); - initApplicationService.init(project); - mockMvc - .perform(post("/api/clients/vue").contentType(MediaType.APPLICATION_JSON).content(TestUtils.convertObjectToJsonBytes(projectDTO))) + .perform( + post("/api/clients/vue/stores/pinia") + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtils.convertObjectToJsonBytes(projectDTO)) + ) .andExpect(status().isOk()); - mockMvc .perform( post("/api/clients/vue/stores/pinia") @@ -65,6 +50,8 @@ void shouldAddPinia() throws Exception { .content(TestUtils.convertObjectToJsonBytes(projectDTO)) ) .andExpect(status().isOk()); - VueAssert.assertPiniaDependency(project); + mockMvc + .perform(post("/api/clients/vue/jwt").contentType(MediaType.APPLICATION_JSON).content(TestUtils.convertObjectToJsonBytes(projectDTO))) + .andExpect(status().isOk()); } } diff --git a/src/test/resources/generator/command/package-pinia.json b/src/test/resources/generator/command/package-pinia.json new file mode 100644 index 00000000000..dfe9bf70656 --- /dev/null +++ b/src/test/resources/generator/command/package-pinia.json @@ -0,0 +1,19 @@ +{ + "name": "jhlitetest", + "version": "0.0.1", + "scripts": { + "prettier:check": "prettier --check \"{,src/**/}*.{md,json,yml,html,js,ts,tsx,css,scss,vue,java,xml}\"", + "prettier:format": "prettier --write \"{,src/**/}*.{md,json,yml,html,js,ts,tsx,css,scss,vue,java,xml}\"" + }, + "devDependencies": { + "prettier-plugin-java": "1.6.1" + }, + "dependencies": { + "axios": "0.26.1", + "pinia": "2.0.13" + }, + "engines": { + "node": ">=14.18.1" + }, + "cacheDirectories": ["node_modules"] +} diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh index 44392e8d244..bc2328829d2 100755 --- a/tests-ci/generate.sh +++ b/tests-ci/generate.sh @@ -238,6 +238,7 @@ elif [[ $application == 'vueapp' ]]; then callApi "/api/developer-tools/frontend-maven-plugin" callApi "/api/clients/vue" callApi "/api/clients/vue/stores/pinia" + callApi "/api/clients/vue/jwt" callApi "/api/clients/cypress" elif [[ $application == 'svelteapp' ]]; then