Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/client/cypress/e2e/specs/admin/projects.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ describe('Administration projects', () => {
.its('response.statusCode')
.should('match', /^20\d$/)

cy.getByDataTestid('tableAdministrationProjects').within(() => {
cy.get('tr').contains(project.name)
.click()
})
cy.getByDataTestid('test-tab-team').click()

cy.getByDataTestid('teamTable').get('tr').contains('Propriétaire')
.should('have.length', 1)
.parent()
Expand All @@ -317,6 +323,12 @@ describe('Administration projects', () => {
.its('response.statusCode')
.should('match', /^20\d$/)

cy.getByDataTestid('tableAdministrationProjects').within(() => {
cy.get('tr').contains(project.name)
.click()
})
cy.getByDataTestid('test-tab-team').click()

cy.getByDataTestid('teamTable').get('tr').contains('Propriétaire')
.should('have.length', 1)
.parent()
Expand Down
21 changes: 0 additions & 21 deletions apps/client/cypress/e2e/specs/admin/system-settings.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ describe('Administration system settings', () => {
const contactEmail = Cypress.env('CONTACT_EMAIL') || 'cloudpinative-relations@interieur.gouv.fr'

beforeEach(() => {
cy.intercept('GET', 'api/v1/system/settings?key=maintenance').as('listMaintenanceSetting')
cy.intercept('GET', 'api/v1/system/settings').as('listSystemSettings')
cy.intercept('POST', 'api/v1/system/settings').as('upsertSystemSetting')

Expand Down Expand Up @@ -52,19 +51,6 @@ describe('Administration system settings', () => {
cy.getByDataTestid('maintenance-notice')
.should('not.exist')
cy.visit('/projects')
// TODO à creuser : La requête est faite deux fois
// la première renvoie "off" alors qu'en bdd la valeur est à "on"
cy.wait('@listMaintenanceSetting').its('response').then(($response) => {
cy.log(JSON.stringify($response?.body))
})
cy.wait('@listMaintenanceSetting').its('response').then(($response) => {
cy.log(JSON.stringify($response?.body))
expect($response?.statusCode).to.match(/^20\d$/)
expect(JSON.stringify($response?.body)).to.equal(JSON.stringify([{
key: 'maintenance',
value: 'on',
}]))
})
cy.wait('@listRoles')
cy.getByDataTestid('maintenance-notice')
.should('be.visible')
Expand Down Expand Up @@ -93,13 +79,6 @@ describe('Administration system settings', () => {
})

cy.visit('/projects')
cy.wait('@listMaintenanceSetting').its('response').then(($response) => {
expect($response?.statusCode).to.match(/^20\d$/)
expect(JSON.stringify($response?.body)).to.equal(JSON.stringify([{
key: 'maintenance',
value: 'on',
}]))
})
cy.getByDataTestid('maintenance-notice')
.should('not.exist')
cy.url().should('contain', '/projects')
Expand Down
3 changes: 0 additions & 3 deletions apps/client/cypress/e2e/specs/roles.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,6 @@ describe('Project roles', () => {
cy.getByDataTestid('replayHooksBtn').should('not.exist')
cy.getByDataTestid('showSecretsBtn').should('not.exist')

cy.getByDataTestid('test-tab-resources').click()
cy.getByDataTestid('noEnvsTr').should('exist')
cy.getByDataTestid('noReposTr').should('exist')
cy.getByDataTestid('test-tab-roles').should('exist')
})
})
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/components/ProjectResources.vue
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ async function copyToClipboard(text: string) {
:environment="selectedEnv"
:is-editable="false"
:is-project-locked="project.locked"
:can-manage="canManageEnvs || (AdminAuthorized.isAdmin(userStore.adminPerms) && asProfile === 'admin')"
:can-manage="canManageEnvs || (AdminAuthorized.Manage(userStore.adminPerms) && asProfile === 'admin')"
@put-environment="(environmentUpdate: UpdateEnvironmentBody) => putEnvironment(environmentUpdate, selectedEnv!.id)"
@delete-environment="() => deleteEnvironment(selectedEnv!.id)"
@cancel="selectedEnv = undefined"
Expand Down
6 changes: 6 additions & 0 deletions apps/client/src/components/SelectProject.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
import router, { isInProject, selectedProjectSlug } from '../router/index.js'
import { useUserStore } from '@/stores/user.js'
import { useProjectStore } from '@/stores/project.js'
import { AdminAuthorized } from '@cpn-console/shared'

const projectStore = useProjectStore()
const userStore = useUserStore()

const canCreateProject = computed(() => {
return AdminAuthorized.ManageProjects(userStore.adminPerms)
})

watch(userStore, async () => {
if (userStore.isLoggedIn && !projectStore.myProjects?.length) {
await projectStore.listMyProjects()
Expand Down Expand Up @@ -71,6 +76,7 @@ function selectProject(slug: string) {
:icon-only="!!projectStore.myProjects.length"
:label="!projectStore.myProjects.length ? 'Créer un nouveau projet' : ''"
small
:disabled="!canCreateProject"
@click="() => router.currentRoute.value.name !== 'CreateProject' && router.push({ name: 'CreateProject' })"
/>
</div>
Expand Down
3 changes: 2 additions & 1 deletion apps/client/src/components/SideMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { isInProject } from '../router/index.js'
import { useUserStore } from '@/stores/user.js'
import { useServiceStore } from '@/stores/services-monitor.js'
import { openCDSEnabled } from '@/utils/env.js'
import { AdminAuthorized } from '@cpn-console/shared'

const route = useRoute()
const userStore = useUserStore()
Expand Down Expand Up @@ -148,7 +149,7 @@ onMounted(() => {

<!-- Onglet Administration -->
<DsfrSideMenuListItem
v-if="userStore.adminPerms"
v-if="AdminAuthorized.Manage(userStore.adminPerms)"
v-bind="{
focusFirstAnchor: false,
}"
Expand Down
4 changes: 3 additions & 1 deletion apps/client/src/components/TeamCt.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const props = withDefaults(
const emit = defineEmits<{
refresh: []
leave: []
transfer: []
}>()

const projectStore = useProjectStore()
Expand Down Expand Up @@ -105,7 +106,8 @@ async function removeUserFromProject(userId: string) {
async function transferOwnerShip() {
if (nextOwnerId.value && props.project.members.find(member => member.userId === nextOwnerId.value)) {
await props.project.Commands.update({ ownerId: nextOwnerId.value })
.finally(() => emit('refresh'))
.then(() => emit('transfer'))
.catch(() => emit('refresh'))
}
}
</script>
Expand Down
117 changes: 94 additions & 23 deletions apps/client/src/router/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,102 @@
import { describe, it, expect } from 'vitest'
import { detectProjectslug } from './index.js'
import { describe, it, expect, beforeEach } from 'vitest'
import { faker } from '@faker-js/faker'
import type { ProjectV2 } from '@cpn-console/shared'
import { detectProjectslug, createAppRouter } from './index.js'
import { useUserStore } from '@/stores/user.js'
import { useProjectStore } from '@/stores/project.js'
import { useSystemSettingsStore } from '@/stores/system-settings.js'

setActivePinia(createPinia())
describe('test router functions: detectProjectslug', () => {
const projectStore = useProjectStore()
const slug = 'the-slug'
const uuid = crypto.randomUUID()
projectStore.updateStore([{
slug,
id: uuid,
}])
it('it should return project\'slug with uuid passed', () => {
const slugFound = detectProjectslug({
params: {
slug: uuid,
},
})
expect(slugFound).toEqual(slug)
describe('router index', () => {
beforeEach(() => {
setActivePinia(createPinia())
})

it('it should return project\'slug with slug passed', () => {
const slugFound = detectProjectslug({
params: {
describe('detect project slug helper', () => {
let projectStore: ReturnType<typeof useProjectStore>
let slug: string
let uuid: string

beforeEach(() => {
projectStore = useProjectStore()
slug = faker.lorem.slug()
uuid = faker.string.uuid()
const recent = faker.date.recent()
const ownerId = faker.string.uuid()
const project: ProjectV2 = {
id: uuid,
clusterIds: [],
description: '',
everyonePerms: '0',
name: slug,
slug,
},
locked: false,
owner: {
type: 'human',
id: ownerId,
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
createdAt: recent.toString(),
updatedAt: recent.toString(),
lastLogin: recent.toString(),
},
ownerId,
roles: [],
members: [],
createdAt: recent.toString(),
updatedAt: recent.toString(),
limitless: false,
hprodCpu: faker.number.int({ min: 0, max: 1000 }),
hprodGpu: faker.number.int({ min: 0, max: 1000 }),
hprodMemory: faker.number.int({ min: 0, max: 1000 }),
prodCpu: faker.number.int({ min: 0, max: 1000 }),
prodGpu: faker.number.int({ min: 0, max: 1000 }),
prodMemory: faker.number.int({ min: 0, max: 1000 }),
status: 'created',
lastSuccessProvisionningVersion: null,
}
projectStore.updateStore([project])
})

it('it should return project\'slug with uuid passed', () => {
const slugFound = detectProjectslug({
params: {
slug: uuid,
},
})
expect(slugFound).toEqual(slug)
})

it('it should return project\'slug with slug passed', () => {
const slugFound = detectProjectslug({
params: {
slug,
},
})
expect(slugFound).toEqual(slug)
})
})

describe('navigation with real router instance', () => {
it('renders home and navigates to projects', async () => {
const router = createAppRouter('')
const userStore = useUserStore()
const systemStore = useSystemSettingsStore()

// Ensure global guard does not redirect to /login
// by simulating an authenticated user.
userStore.isLoggedIn = true
userStore.setIsLoggedIn = async () => {}
systemStore.listSystemSettings = async () => {}
router.push('/')
await router.isReady()

expect(router.currentRoute.value.name).toEqual('Home')

await router.push('/projects')
await router.isReady()

expect(router.currentRoute.value.matched.some(r => r.name === 'Projects')).toBe(true)
})
expect(slugFound).toEqual(slug)
})
})
84 changes: 37 additions & 47 deletions apps/client/src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { useSystemSettingsStore } from '@/stores/system-settings.js'

import DsoHome from '@/views/DsoHome.vue'
import NotFound from '@/views/NotFound.vue'
import { swaggerUiPath } from '@cpn-console/shared'
import { AdminAuthorized, swaggerUiPath } from '@cpn-console/shared'
import { uuid } from '@/utils/regex.js'

const AdminCluster = () => import('@/views/admin/AdminCluster.vue')
Expand Down Expand Up @@ -270,53 +270,43 @@ export const routes: Readonly<RouteRecordRaw[]> = [
},
]

const router = createRouter({
history: createWebHistory(import.meta.env?.BASE_URL || ''),
scrollBehavior: (to) => { if (to.hash && !/^#state=/.exec(to.hash)) return ({ el: to.hash }) },
routes,
})

/**
* Set application title
*/
router.beforeEach((to) => { // Cf. https://github.com/vueuse/head pour des transformations avancées de Head
const specificTitle = to.meta.title ? `${to.meta.title} - ` : ''
document.title = `${specificTitle}${MAIN_TITLE}`
})

/**
* Redirect unlogged user to login view
*/
router.beforeEach(async (to, _from, next) => {
const validPath = ['Login', 'Home', 'Doc', 'NotFound', 'ServicesHealth', 'Maintenance', 'Logout', 'Swagger']
const userStore = useUserStore()
const systemStore = useSystemSettingsStore()
await userStore.setIsLoggedIn()

// Redirige sur la page login si le path le requiert et l'utilisateur n'est pas connecté
if (
!validPath.includes(to.name?.toString() ?? '')
&& !userStore.isLoggedIn
) {
return next('/login')
}

// Redirige sur l'accueil si le path est Login et que l'utilisateur est connecté
if (to.name === 'Login' && userStore.isLoggedIn) {
return next('/')
}

// Redirige vers la page maintenance si la maintenance est activée
if (
!validPath.includes(to.name?.toString() ?? '')
&& userStore.isLoggedIn
) {
await systemStore.listSystemSettings('maintenance')
if (systemStore.systemSettingsByKey.maintenance?.value === 'on' && userStore.adminPerms === 0n) return next('/maintenance')
}
export function createAppRouter(base?: string) {
const router = createRouter({
history: createWebHistory(base ?? (import.meta.env?.BASE_URL || '')),
scrollBehavior: (to) => { if (to.hash && !/^#state=/.exec(to.hash)) return ({ el: to.hash }) },
routes,
})
router.beforeEach((to) => {
const specificTitle = to.meta.title ? `${to.meta.title} - ` : ''
document.title = `${specificTitle}${MAIN_TITLE}`
})
router.beforeEach(async (to, _from, next) => {
const validPath = new Set(['Login', 'Home', 'Doc', 'NotFound', 'ServicesHealth', 'Maintenance', 'Logout', 'Swagger'])
const userStore = useUserStore()
const systemStore = useSystemSettingsStore()
await userStore.setIsLoggedIn()
if (
!validPath.has(to.name?.toString() ?? '')
&& !userStore.isLoggedIn
) {
return next('/login')
}
if (to.name === 'Login' && userStore.isLoggedIn) {
return next('/')
}
if (
!validPath.has(to.name?.toString() ?? '')
&& userStore.isLoggedIn
) {
await systemStore.listSystemSettings()
if (systemStore.systemSettingsByKey.maintenance?.value === 'on' && !AdminAuthorized.Manage(userStore.adminPerms)) return next('/maintenance')
}
next()
})
return router
}

next()
})
const router = createAppRouter()

export const isInProject = computed(() => router.currentRoute.value.matched.some(route => route.name === 'Project'))
export const selectedProjectSlug = computed<string | undefined>(() => {
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/stores/admin-role.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const useAdminRoleStore = defineStore('adminRole', () => {
roles.value = await apiClient.AdminRoles.listAdminRoles().then((response: any) =>
extractData(response, 200),
)
if (AdminAuthorized.isAdmin(userStore.adminPerms)) {
if (AdminAuthorized.ListRoles(userStore.adminPerms)) {
await countMembersRoles()
}
return roles.value
Expand Down
Loading
Loading