Skip to content
Open
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
14 changes: 12 additions & 2 deletions apps/client/src/components/ProjectRoleForm.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script lang="ts" setup>
import { computed, ref } from 'vue'
import type { Member, ProjectV2, RoleBigint } from '@cpn-console/shared'
import type { Member, ProjectRoleBigint, ProjectV2 } from '@cpn-console/shared'
import { PROJECT_PERMS, projectPermsDetails, shallowEqual } from '@cpn-console/shared'

const props = defineProps<{
Expand All @@ -10,19 +10,21 @@ const props = defineProps<{
allMembers: Member[]
projectId: ProjectV2['id']
isEveryone: boolean
oidcGroup?: string
}>()

defineEmits<{
delete: []
updateMemberRoles: [checked: boolean, userId: Member['userId']]
save: [value: Omit<RoleBigint, 'position'>]
save: [value: Omit<ProjectRoleBigint, 'position' | 'projectId'>]
cancel: []
}>()
const router = useRouter()
const role = ref({
...props,
permissions: props.permissions ?? 0n,
allMembers: props.allMembers ?? [],
oidcGroup: props.oidcGroup ?? '',
})

const isUpdated = computed(() => {
Expand Down Expand Up @@ -68,6 +70,14 @@ function updateChecked(checked: boolean, value: bigint) {
class="mb-5"
:disabled="role.isEveryone"
/>
<h6>Groupe OIDC</h6>
<DsfrInput
v-model="role.oidcGroup"
data-testid="roleOidcGroupInput"
label-visible
class="mb-5"
:disabled="role.isEveryone"
/>
<h6>Permissions</h6>
<div
v-for="scope in projectPermsDetails"
Expand Down
9 changes: 6 additions & 3 deletions apps/client/src/components/ProjectRoles.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts" setup>
import type { Member, Role, RoleBigint } from '@cpn-console/shared'
import type { Member, ProjectRole, ProjectRoleBigint, Role, RoleBigint } from '@cpn-console/shared'
import { useSnackbarStore } from '@/stores/snackbar.js'
import type { Project } from '@/utils/project-utils.js'

Expand All @@ -11,7 +11,7 @@ const snackbarStore = useSnackbarStore()

const selectedId = ref<string>()

type RoleItem = Omit<Role, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }
type RoleItem = Omit<ProjectRole, 'permissions'> & { permissions: bigint, memberCounts: number, isEveryone: boolean }

const roleList = ref<RoleItem[]>([])

Expand Down Expand Up @@ -56,7 +56,7 @@ async function saveEveryoneRole(role: { permissions: bigint }) {
snackbarStore.setMessage('Rôle mis à jour', 'success')
}

async function saveRole(role: Omit<RoleBigint, 'position'>) {
async function saveRole(role: Omit<ProjectRoleBigint, 'position' | 'projectId'>) {
if (role.id === 'everyone') {
await saveEveryoneRole(role)
snackbarStore.setMessage('Rôle mis à jour', 'success')
Expand All @@ -67,6 +67,7 @@ async function saveRole(role: Omit<RoleBigint, 'position'>) {
id: selectedRole.value.id,
permissions: role.permissions.toString(),
name: role.name,
oidcGroup: role.oidcGroup,
}])
reload()
snackbarStore.setMessage('Rôle mis à jour', 'success')
Expand All @@ -86,6 +87,7 @@ function reload() {
permissions: BigInt(props.project.everyonePerms),
position: 1000,
isEveryone: true,
projectId: props.project.id,
})
roleList.value = roles
}
Expand Down Expand Up @@ -142,6 +144,7 @@ watch(props.project, reload, { immediate: true })
:permissions="BigInt(selectedRole.permissions)"
:project-id="project.id"
:is-everyone="selectedRole.isEveryone"
:oidc-group="selectedRole.oidcGroup"
:all-members="project.members"
@delete="deleteRole(selectedRole.id)"
@update-member-roles="(checked: boolean, userId: Member['userId']) => updateMember(checked, userId)"
Expand Down
2 changes: 1 addition & 1 deletion apps/client/src/utils/project-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export class Project implements ProjectV2 {
locked: boolean
owner: Omit<User, 'adminRoleIds'>
ownerId: string
roles: { id: string, name: string, permissions: string, position: number }[]
roles: { id: string, name: string, permissions: string, position: number, projectId: string, oidcGroup?: string }[]
members: ({ userId: string, firstName: string, lastName: string, email: string, roleIds: string[] } | { updatedAt: string, createdAt: string, firstName: string, lastName: string, email: string, userId: string, roleIds: string[] })[]
createdAt: string
updatedAt: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ProjectRole" ADD COLUMN "oidcGroup" TEXT NOT NULL DEFAULT '';
2 changes: 1 addition & 1 deletion apps/server/src/prisma/migrations/migration_lock.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"
provider = "postgresql"
1 change: 1 addition & 0 deletions apps/server/src/prisma/schema/project.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ model ProjectRole {
permissions BigInt
projectId String @db.Uuid
position Int @db.SmallInt
oidcGroup String @default("")
project Project @relation(fields: [projectId], references: [id])
}

Expand Down
116 changes: 88 additions & 28 deletions apps/server/src/resources/admin-role/business.spec.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,74 @@
import { faker } from '@faker-js/faker'
import { describe, expect, it } from 'vitest'
import type { AdminRole, User } from '@prisma/client'
import { faker } from '@faker-js/faker'
import prisma from '../../__mocks__/prisma.js'
import { BadRequest400 } from '../../utils/errors.ts'
import { countRolesMembers, createRole, deleteRole, listRoles, patchRoles } from './business.ts'

describe('test admin-role business', () => {
describe('listRoles', () => {
it('should stringify bigint', async () => {
const partialRole: Partial<AdminRole> = {
const dbRole: AdminRole = {
id: faker.string.uuid(),
name: faker.string.alphanumeric(),
permissions: 4n,
position: 0,
oidcGroup: '',
}

prisma.adminRole.findMany.mockResolvedValueOnce([partialRole])
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
const response = await listRoles()
expect(response).toEqual([{ permissions: '4' }])
expect(response).toContainEqual(expect.objectContaining({ permissions: '4' }))
})
})

describe('createRole', () => {
it('should create role with incremented position when position 0 is the highest', async () => {
const dbRole: Partial<AdminRole> = {
const dbRole: AdminRole = {
id: faker.string.uuid(),
name: faker.string.alphanumeric(),
permissions: 4n,
position: 0,
oidcGroup: '',
}

prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
prisma.adminRole.create.mockResolvedValue(null)
prisma.adminRole.create.mockResolvedValue(dbRole)
await createRole({ name: 'test' })

expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 1 } })
})

it('should create role with incremented position with bigger position', async () => {
const dbRole: Partial<AdminRole> = {
const dbRole: AdminRole = {
id: faker.string.uuid(),
name: faker.string.alphanumeric(),
permissions: 4n,
position: 50,
oidcGroup: '',
}

prisma.adminRole.findFirst.mockResolvedValueOnce(dbRole)
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
prisma.adminRole.create.mockResolvedValue(null)
prisma.adminRole.create.mockResolvedValue(dbRole)
await createRole({ name: 'test' })

expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 51 } })
})

it('should create role with incremented position with no role in db', async () => {
const dbRole: Partial<AdminRole> = {
const dbRole: AdminRole = {
id: faker.string.uuid(),
name: faker.string.alphanumeric(),
permissions: 4n,
position: 50,
oidcGroup: '',
}

prisma.adminRole.findFirst.mockResolvedValueOnce(undefined)
prisma.adminRole.findFirst.mockResolvedValueOnce(null)
prisma.adminRole.findMany.mockResolvedValueOnce([dbRole])
prisma.adminRole.create.mockResolvedValue(null)
prisma.adminRole.create.mockResolvedValue(dbRole)
await createRole({ name: 'test' })

expect(prisma.adminRole.create).toHaveBeenCalledWith({ data: { name: 'test', permissions: 0n, position: 0 } })
Expand All @@ -65,16 +78,39 @@ describe('test admin-role business', () => {
const roleId = faker.string.uuid()
it('should delete role and remove id from concerned users', async () => {
const users = [{
adminRoleIds: [roleId],
id: faker.string.uuid(),
type: 'human',
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
adminRoleIds: [roleId],
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
lastLogin: faker.date.past(),
}, {
adminRoleIds: [roleId, faker.string.uuid()],
id: faker.string.uuid(),
}] as const satisfies Partial<User>[]
type: 'human',
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
adminRoleIds: [roleId, faker.string.uuid()],
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
lastLogin: faker.date.past(),
}] as const satisfies User[]

const dbRole: AdminRole = {
name: 'Admin',
id: roleId,
permissions: 4n,
position: 50,
oidcGroup: '',
}

prisma.user.findMany.mockResolvedValueOnce(users)
prisma.adminRole.findMany.mockResolvedValueOnce([])
prisma.adminRole.create.mockResolvedValue(null)
prisma.adminRole.findUnique.mockResolvedValueOnce(dbRole)
prisma.adminRole.create.mockResolvedValue(dbRole)
await deleteRole(roleId)

expect(prisma.user.update).toHaveBeenNthCalledWith(1, { where: { id: users[0].id }, data: { adminRoleIds: [] } })
Expand All @@ -84,35 +120,59 @@ describe('test admin-role business', () => {
})
describe('countRolesMembers', () => {
it('should return aggregated role member counts', async () => {
const partialRoles = [{
const roles = [{
id: faker.string.uuid(),
name: faker.string.alphanumeric(),
oidcGroup: '',
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
position: 0,
}, {
id: faker.string.uuid(),
}] as const satisfies Partial<AdminRole>[]
name: faker.string.alphanumeric(),
oidcGroup: '',
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
position: 1,
}] as const satisfies AdminRole[]

const users = [{
adminRoleIds: [partialRoles[0].id, partialRoles[1].id],
id: faker.string.uuid(),
type: 'human',
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
adminRoleIds: [roles[0].id, roles[1].id],
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
lastLogin: faker.date.past(),
}, {
adminRoleIds: [partialRoles[1].id],
}] as const satisfies Partial<User>[]
prisma.adminRole.findMany.mockResolvedValue(partialRoles)
id: faker.string.uuid(),
type: 'human',
firstName: faker.person.firstName(),
lastName: faker.person.lastName(),
email: faker.internet.email(),
adminRoleIds: [roles[1].id],
createdAt: faker.date.past(),
updatedAt: faker.date.recent(),
lastLogin: faker.date.past(),
}] as const satisfies User[]
prisma.adminRole.findMany.mockResolvedValue(roles)
prisma.user.findMany.mockResolvedValue(users)

const response = await countRolesMembers()

expect(response).toEqual({ [partialRoles[0].id]: 1, [partialRoles[1].id]: 2 })
expect(response).toEqual({ [roles[0].id]: 1, [roles[1].id]: 2 })
})
})
describe('patchRoles', () => {
const dbRoles: AdminRole[] = [{
id: faker.string.uuid(),
name: faker.company.name(),
name: faker.string.alphanumeric(),
oidcGroup: '',
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
position: 0,
}, {
id: faker.string.uuid(),
name: faker.company.name(),
name: faker.string.alphanumeric(),
oidcGroup: '',
permissions: faker.number.bigInt({ min: 0n, max: 50000n }),
position: 1,
Expand All @@ -125,7 +185,7 @@ describe('test admin-role business', () => {
})

it('should return 400 if incoherent positions', async () => {
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
{ id: dbRoles[0].id, position: 1 },
{ id: dbRoles[1].id, position: 1 },
]
Expand All @@ -137,7 +197,7 @@ describe('test admin-role business', () => {
expect(prisma.adminRole.update).toHaveBeenCalledTimes(0)
})
it('should return 400 if incoherent positions (missing roles)', async () => {
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
{ id: dbRoles[1].id, position: 1 },
]
prisma.adminRole.findMany.mockResolvedValue(dbRoles)
Expand All @@ -148,7 +208,7 @@ describe('test admin-role business', () => {
expect(prisma.adminRole.update).toHaveBeenCalledTimes(0)
})
it('should update positions', async () => {
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
const updateRoles: Pick<AdminRole, 'id' | 'position'>[] = [
{ id: dbRoles[0].id, position: 1 },
{ id: dbRoles[1].id, position: 0 },
]
Expand All @@ -159,7 +219,7 @@ describe('test admin-role business', () => {
expect(prisma.adminRole.update).toHaveBeenCalledTimes(2)
})
it('should update permissions', async () => {
const updateRoles: Pick<AdminRole, 'id' | 'position'> = [
const updateRoles: (Pick<AdminRole, 'id'> & { permissions?: string })[] = [
{ id: dbRoles[1].id, permissions: '0' },
]
prisma.adminRole.findMany.mockResolvedValue(dbRoles)
Expand Down
Loading
Loading