diff --git a/backend/package-lock.json b/backend/package-lock.json index 25a8174d..243b8e9e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6,6 +6,7 @@ "": { "license": "Apache-2.0", "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/cli": "^10.1.16", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.0.0", @@ -16,6 +17,7 @@ "@nestjs/terminus": "^10.2.1", "@nestjs/testing": "^10.0.0", "@prisma/client": "^5.7.0", + "axios": "^1.7.2", "dotenv": "^16.0.1", "express-prom-bundle": "^7.0.0", "helmet": "^7.0.0", @@ -23,6 +25,7 @@ "nestjs-prisma": "^0.23.0", "pg": "^8.11.3", "prom-client": "^15.1.0", + "react-redux": "^9.1.2", "reflect-metadata": "^0.2.0", "rimraf": "^5.0.0", "rxjs": "^7.8.0", @@ -1623,6 +1626,17 @@ "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.14.2.tgz", "integrity": "sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==" }, + "node_modules/@nestjs/axios": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.2.tgz", + "integrity": "sha512-Z6GuOUdNQjP7FX+OuV2Ybyamse+/e0BFdTWBX5JxpBDKA+YkdLynDgG6HTF04zy6e9zPa19UX0WA2VDoehwhXQ==", + "license": "MIT", + "peerDependencies": { + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", + "rxjs": "^6.0.0 || ^7.0.0" + } + }, "node_modules/@nestjs/cli": { "version": "10.3.2", "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.3.2.tgz", @@ -2620,6 +2634,12 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz", + "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.32", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", @@ -3301,8 +3321,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/available-typed-arrays": { "version": "1.0.7", @@ -3319,6 +3338,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/axios": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", + "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -4072,7 +4102,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -4412,7 +4441,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -5716,6 +5744,26 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -5791,7 +5839,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -8192,6 +8239,19 @@ "node": ">=0.1.90" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -9318,6 +9378,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -9406,12 +9472,48 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/react-redux": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.1.2.tgz", + "integrity": "sha512-0OA4dhM1W48l3uzmv6B7TXPCGmokUU4p1M44DGN2/D9a1FjVPukVjER1PcPX97jIg6aUeLq1XJo1IpfbgULn0w==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.3", + "use-sync-external-store": "^1.0.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25", + "react": "^18.0", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -11069,6 +11171,15 @@ "node": ">=6.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/backend/package.json b/backend/package.json index 6e52c952..9ae9ecba 100644 --- a/backend/package.json +++ b/backend/package.json @@ -22,6 +22,7 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/cli": "^10.1.16", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.0.0", @@ -32,6 +33,7 @@ "@nestjs/terminus": "^10.2.1", "@nestjs/testing": "^10.0.0", "@prisma/client": "^5.7.0", + "axios": "^1.7.2", "dotenv": "^16.0.1", "express-prom-bundle": "^7.0.0", "helmet": "^7.0.0", @@ -39,6 +41,7 @@ "nestjs-prisma": "^0.23.0", "pg": "^8.11.3", "prom-client": "^15.1.0", + "react-redux": "^9.1.2", "reflect-metadata": "^0.2.0", "rimraf": "^5.0.0", "rxjs": "^7.8.0", diff --git a/backend/src/admin/admin.controller.spec.ts b/backend/src/admin/admin.controller.spec.ts new file mode 100644 index 00000000..0b8ca902 --- /dev/null +++ b/backend/src/admin/admin.controller.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminController } from './admin.controller'; + +describe('AdminController', () => { + let controller: AdminController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [AdminController], + }).compile(); + + controller = module.get(AdminController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); +}); diff --git a/backend/src/admin/admin.controller.ts b/backend/src/admin/admin.controller.ts new file mode 100644 index 00000000..3c91c44e --- /dev/null +++ b/backend/src/admin/admin.controller.ts @@ -0,0 +1,12 @@ +import { Controller, Get } from "@nestjs/common"; +import { AdminService } from "./admin.service"; + +@Controller("admin") +export class AdminController { + constructor(private readonly adminService: AdminService) {} + + @Get() + findAll(): Promise { + return this.adminService.findAll(); + } +} diff --git a/backend/src/admin/admin.module.ts b/backend/src/admin/admin.module.ts new file mode 100644 index 00000000..bd4ffd03 --- /dev/null +++ b/backend/src/admin/admin.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { AdminController } from "./admin.controller"; +import { AdminService } from "./admin.service"; +import { HttpModule } from "@nestjs/axios"; + +@Module({ + imports: [HttpModule], + controllers: [AdminController], + providers: [AdminService], +}) +export class AdminModule {} diff --git a/backend/src/admin/admin.service.spec.ts b/backend/src/admin/admin.service.spec.ts new file mode 100644 index 00000000..5e5e153d --- /dev/null +++ b/backend/src/admin/admin.service.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { AdminService } from './admin.service'; + +describe('AdminService', () => { + let service: AdminService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [AdminService], + }).compile(); + + service = module.get(AdminService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); +}); diff --git a/backend/src/admin/admin.service.ts b/backend/src/admin/admin.service.ts new file mode 100644 index 00000000..2a91b912 --- /dev/null +++ b/backend/src/admin/admin.service.ts @@ -0,0 +1,54 @@ +import { HttpService } from "@nestjs/axios"; +import { Injectable } from "@nestjs/common"; +import { firstValueFrom } from "rxjs"; + +@Injectable() +export class AdminService { + constructor(private readonly httpService: HttpService) {} + + /** + * Gets a list of all ??? users + * + * @returns all ??? users + */ + async findAll(): Promise { + const bearerToken = await this.getToken(); + const role = "Enmods Admin"; + const url = `${process.env.users_api_base_url}/integrations/${process.env.integration_id}/${process.env.css_environment}/roles/${role}/users`; + const config = { + headers: { Authorization: "Bearer " + bearerToken }, + }; + try { + console.log("trying user api"); + const response = await firstValueFrom(this.httpService.get(url, config)); + console.log(response); + return response.data.data; + } catch (err) { + console.log(err.response?.data || err.message); + throw err; + } + } + + async getToken() { + const url = process.env.users_api_token_url; + const token = `${process.env.users_api_client_id}:${process.env.users_api_client_secret}`; + const encodedToken = Buffer.from(token).toString("base64"); + const config = { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: "Basic " + encodedToken, + }, + }; + const grantTypeParam = new URLSearchParams(); + grantTypeParam.append("grant_type", "client_credentials"); + try { + const response = await firstValueFrom( + this.httpService.post(url, grantTypeParam.toString(), config) + ); + return response.data.access_token; + } catch (error) { + console.log(error.response?.data || error.message); + throw error; + } + } +} diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 472e5da0..06ed4570 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -9,6 +9,7 @@ import { AppController } from "./app.controller"; import { MetricsController } from "./metrics.controller"; import { TerminusModule } from '@nestjs/terminus'; import { HealthController } from "./health.controller"; +import { AdminModule } from './admin/admin.module'; const DB_HOST = process.env.POSTGRES_HOST || "localhost"; const DB_USER = process.env.POSTGRES_USER || "postgres"; @@ -45,7 +46,8 @@ function getMiddlewares() { middlewares: getMiddlewares(), }, }), - UsersModule + UsersModule, + AdminModule ], controllers: [AppController,MetricsController, HealthController], providers: [AppService] diff --git a/frontend/.editorconfig b/frontend/.editorconfig new file mode 100644 index 00000000..cef73e62 --- /dev/null +++ b/frontend/.editorconfig @@ -0,0 +1,10 @@ +# .editorconfig +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/frontend/cypress.config.ts b/frontend/cypress.config.ts index 732c2df9..0116ae19 100644 --- a/frontend/cypress.config.ts +++ b/frontend/cypress.config.ts @@ -1,12 +1,13 @@ -import { defineConfig } from "cypress"; +import { defineConfig } from 'cypress' export default defineConfig({ e2e: { - baseUrl: 'https://quickstart-openshift-test-frontend.apps.silver.devops.gov.bc.ca/', + baseUrl: + 'https://quickstart-openshift-test-frontend.apps.silver.devops.gov.bc.ca/', setupNodeEvents(on, config) { // implement node event listeners here }, experimentalStudio: true, experimentalWebKitSupport: true, }, -}); +}) diff --git a/frontend/cypress/e2e/home-page.cy.ts b/frontend/cypress/e2e/home-page.cy.ts index 3d7864db..e130a5df 100644 --- a/frontend/cypress/e2e/home-page.cy.ts +++ b/frontend/cypress/e2e/home-page.cy.ts @@ -1,8 +1,7 @@ /// -describe("Home page visit", () => { - - it("visit landing page", () => { - cy.visit("/"); - cy.contains("Quickstart OpenShift"); - }); -}); +describe('Home page visit', () => { + it('visit landing page', () => { + cy.visit('/') + cy.contains('Quickstart OpenShift') + }) +}) diff --git a/frontend/cypress/e2e/user-table.cy.ts b/frontend/cypress/e2e/user-table.cy.ts index 8f87e5b2..1f680d76 100644 --- a/frontend/cypress/e2e/user-table.cy.ts +++ b/frontend/cypress/e2e/user-table.cy.ts @@ -1,29 +1,45 @@ /// -describe("User Table", () => { +describe('User Table', () => { beforeEach(() => { - cy.visit("/"); - }); + cy.visit('/') + }) - it("renders the table", () => { - cy.get(".MuiDataGrid-root").should("exist").should(($div) => { - // access the native DOM element - expect($div.get(0).innerText).exist - }); - cy.get("div.MuiDataGrid-columnHeader:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)").should("exist").should(($div) => { - // access the native DOM element - expect($div.get(0).innerText).to.eq('Employee ID'); - }); - cy.get("div.MuiDataGrid-columnHeader:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)").should("exist").should(($div) => { - // access the native DOM element - expect($div.get(0).innerText).to.eq('Employee Name'); - }); - cy.get("div.MuiDataGrid-columnHeader:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)").should("exist").should(($div) => { - // access the native DOM element - expect($div.get(0).innerText).to.eq('Employee Email'); - }); - cy.get(".MuiTablePagination-displayedRows").should("exist").should(($div) => { - // access the native DOM element - expect($div.get(0).innerText).to.contains('of 5'); - }); - }); -}); + it('renders the table', () => { + cy.get('.MuiDataGrid-root') + .should('exist') + .should(($div) => { + // access the native DOM element + expect($div.get(0).innerText).exist + }) + cy.get( + 'div.MuiDataGrid-columnHeader:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)', + ) + .should('exist') + .should(($div) => { + // access the native DOM element + expect($div.get(0).innerText).to.eq('Employee ID') + }) + cy.get( + 'div.MuiDataGrid-columnHeader:nth-child(2) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)', + ) + .should('exist') + .should(($div) => { + // access the native DOM element + expect($div.get(0).innerText).to.eq('Employee Name') + }) + cy.get( + 'div.MuiDataGrid-columnHeader:nth-child(3) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1) > div:nth-child(1)', + ) + .should('exist') + .should(($div) => { + // access the native DOM element + expect($div.get(0).innerText).to.eq('Employee Email') + }) + cy.get('.MuiTablePagination-displayedRows') + .should('exist') + .should(($div) => { + // access the native DOM element + expect($div.get(0).innerText).to.contains('of 5') + }) + }) +}) diff --git a/frontend/cypress/support/index.ts b/frontend/cypress/support/index.ts index 551c38a4..9edde2d8 100644 --- a/frontend/cypress/support/index.ts +++ b/frontend/cypress/support/index.ts @@ -1,2 +1 @@ -declare namespace Cypress { -} +declare namespace Cypress {} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index b8e7406b..d4020884 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -3,6 +3,7 @@ import Header from '@/components/Header' import Footer from '@/components/Footer' import AppRoutes from '@/routes' import { BrowserRouter } from 'react-router-dom' +import Sidebar from './components/Sidebar' const styles = { container: { @@ -10,28 +11,50 @@ const styles = { flexDirection: 'column', minHeight: '100vh', }, - content: { - flexGrow: 1, - marginTop: '5em', - marginRight: '1em', - marginLeft: '1em', - marginBottom: '5em', - height: '100%', + contentWrapper: { display: 'flex', justifyContent: 'center', alignItems: 'flex-start', + + width: '100%', + }, + content: { + display: 'flex', + width: '1200px', + }, + sidebar: { + marginTop: '8em', + // width: '28%', + width: '20%', + }, + mainContent: { + marginTop: '8em', + width: '70%', + }, + separator: { + width: '1px', + bgcolor: 'rgb(217, 217, 217)', }, } + export default function App() { return ( - -
- - - - + + +
+ + + + + + + + + + + +