Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/ps/assign user roles backend #311

Open
wants to merge 16 commits into
base: feature/permissioning-system
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
107 changes: 92 additions & 15 deletions app/src/components/NotificationDropdown.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@
<li v-for="notification in paginatedNotifications" :key="notification.id">
<a
@click="updateNotification(notification)"
:href="isInvitation(notification) ? `/${notification.resource}` : `#`"
:href="getResource(notification)[0] === `teams` ? `/${notification.resource}` : `#`"
>
<div class="notification__body">
<span :class="{ 'font-bold': !notification.isRead }">
{{ notification.message }}
</span>
</div>
<!--<div class="notification__footer">{{ notification.author }} {{ notification.createdAt }}</div>-->
</a>
</li>
<!-- Pagination Controls -->
Expand Down Expand Up @@ -57,25 +56,25 @@ import { useCustomFetch } from '@/composables/useCustomFetch'
import { BellIcon } from '@heroicons/vue/24/outline'
import { ChevronLeftIcon } from '@heroicons/vue/24/outline'
import { ChevronRightIcon } from '@heroicons/vue/24/outline'
import { useUserDataStore } from "@/stores/user"
import { log, parseError } from "@/utils";

const currentPage = ref(1)
const itemsPerPage = ref(4)
const totalPages = ref(0)

const updateEndPoint = ref('')
const endpointUrl = ref('')

const {
//isFetching: isNotificationsFetching,
//error: notificationError,
data: notifications,
execute: executeFetchNotifications
} = useCustomFetch<NotificationResponse>('notification').json()
} = useCustomFetch<NotificationResponse>('notification')
.get()
.json()

const {
//isFetching: isUpdateNotificationsFetching,
//error: isUpdateNotificationError,
execute: executeUpdateNotifications
//data: _notifications
} = useCustomFetch<NotificationResponse>(updateEndPoint, {
} = useCustomFetch<NotificationResponse>(endpointUrl, {
immediate: false
})
.put()
Expand All @@ -91,22 +90,100 @@ const isUnread = computed(() => {
return idx > -1
})

const isInvitation = (notification: Notification) => {
const getResource = (notification: Notification) => {
if (notification.resource) {
const resourceArr = notification.resource.split('/')
if (resourceArr[0] === 'teams') return true
return resourceArr
} else return []
}

const {
execute: executeFetchMemberContract,
data: memberContract
} = useCustomFetch(endpointUrl, {
immediate: false,
beforeFetch: async ({ options, url, cancel }) => {
options.headers = {
memberaddress: `${useUserDataStore().address}`,
'Content-Type': 'application/json',
...options.headers
}
return { options, url, cancel }
}
})
.get()
.json()

return false
}
const signature = ref('')

const {
execute: executeAddMemberSignature
} = useCustomFetch(endpointUrl, {
immediate: false
})
.put()
.json()

const updateNotification = async (notification: Notification) => {
updateEndPoint.value = `notification/${notification.id}`
const resource = getResource(notification)
if (resource[0] === `role-assignment`) {
endpointUrl.value = `teams/${resource[1]}/member/contract`
//get contract
await executeFetchMemberContract()
const contract = JSON
.parse(JSON.parse(memberContract.value.contract))
//sign contract
signature.value = await signContract(contract)
//save signature
endpointUrl.value = `teams/${resource[1]}/member/signature/${signature.value}`
await executeAddMemberSignature()
}
endpointUrl.value = `notification/${notification.id}`

await executeUpdateNotifications()
await executeFetchNotifications()
}

const signContract = async (contract: undefined | Object) => {
if (!contract) return
const params = [
useUserDataStore().address,
{
types: {
EIP712Domain: [
{ name: "name", type: "string" },
{ name: "version", type: "string" }
],
Entitlement: [
{ name: "name", type: "string" },
{ name: "resource", type: "string" },
{ name: "accessLevel", type: "string" }
],
Role: [
{ name: "name", type: "string" },
{ name: "entitlement", type: "Entitlement" }
],
Contract: [
{ name: "assignedTo", type: "address" },
{ name: "assignedBy", type: "address" },
{ name: "role", type: "Role" }
]
},
primaryType: "Contract",
domain: {
"name": "CNC Contract",
"version": "1"
},
message: contract
}
]
try {
return await (window as any).ethereum.request({method: "eth_signTypedData_v4", params: params})
} catch (error) {
log.error(parseError(error))
}
}

const paginatedNotifications = computed(() => {
if (!notifications.value?.data) return []
const start = (currentPage.value - 1) * itemsPerPage.value
Expand Down
21 changes: 19 additions & 2 deletions app/src/components/sections/SingleTeamView/MemberSection.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
/>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ref, watch, onMounted } from 'vue'
import { useCustomFetch } from '@/composables/useCustomFetch'
import MemberCard from '@/components/sections/SingleTeamView/MemberCard.vue'
import AddMemberCard from '@/components/sections/SingleTeamView/AddMemberCard.vue'
Expand All @@ -57,7 +57,7 @@ const { addSuccessToast, addErrorToast } = useToastStore()

const route = useRoute()

defineProps(['team', 'teamIsFetching'])
const { team, teamIsFetching } = defineProps(['team', 'teamIsFetching'])
const emits = defineEmits(['getTeam'])
// useFetch instance for adding members to team
const {
Expand Down Expand Up @@ -229,6 +229,16 @@ const signContract = async (contract: undefined | Object) => {
}
}

const memberRolesData = ref<{}>()

const {
execute: executeCreateMemberRoles
} = useCustomFetch(`teams/${team.id}/member/add-roles`, {
immediate: false
})
.post(memberRolesData)
.json()

const addRoles = async (member: Partial<MemberInput>) => {
isAddingRole.value = true
if (v$.value.$errors.length > 0) {
Expand All @@ -241,6 +251,13 @@ const addRoles = async (member: Partial<MemberInput>) => {
console.log(`member.roles: `, member.roles)
console.log(`signature: `, signature)
console.log(`contract: `, JSON.stringify(contract))
memberRolesData.value = {
member,
signature,
contract: JSON.stringify(contract)
}
console.log(`memberRolesData: `, memberRolesData.value)
await executeCreateMemberRoles()
isAddingRole.value = false
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
-- AlterTable
ALTER TABLE "UserRoleEntitlement" ALTER COLUMN "value" SET DATA TYPE TEXT;

-- CreateTable
CREATE TABLE "MemberTeamsData" (
"id" SERIAL NOT NULL,
"userAddress" TEXT NOT NULL,
"teamId" INTEGER NOT NULL,
"contract" TEXT,
"memberSignature" TEXT,
"ownerSignature" TEXT,

CONSTRAINT "MemberTeamsData_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "MemberTeamsData_userAddress_teamId_key" ON "MemberTeamsData"("userAddress", "teamId");

-- AddForeignKey
ALTER TABLE "MemberTeamsData" ADD CONSTRAINT "MemberTeamsData_userAddress_fkey" FOREIGN KEY ("userAddress") REFERENCES "User"("address") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "MemberTeamsData" ADD CONSTRAINT "MemberTeamsData_teamId_fkey" FOREIGN KEY ("teamId") REFERENCES "Team"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
17 changes: 16 additions & 1 deletion backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ model User {
ownedTeams Team[] @relation("OwnedTeams")
notifications Notification[]
roles UserRole[]
memberTeamsData MemberTeamsData[]
}

model Team {
Expand All @@ -33,6 +34,20 @@ model Team {
name String @unique
bankAddress String? // A team can have no bank contract
votingAddress String?
memberTeamsData MemberTeamsData[]
}

model MemberTeamsData {
id Int @id @default(autoincrement()) // Add an ID for easy referencing
user User @relation(fields: [userAddress], references: [address])
team Team @relation(fields: [teamId], references: [id])
userAddress String
teamId Int
contract String?
memberSignature String?
ownerSignature String?

@@unique([userAddress, teamId])
}

model Notification {
Expand Down Expand Up @@ -109,7 +124,7 @@ model UserRoleEntitlement {
id Int @id @default(autoincrement())
userRoleId Int
entitlementId Int
value Float? // Unique value for each user for the entitlement
value String? // Unique value for each user for the entitlement
lastPayDate DateTime? @default(now())

userRole UserRole @relation(fields: [userRoleId], references: [id])
Expand Down
119 changes: 119 additions & 0 deletions backend/src/controllers/teamController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,122 @@ const buildFilterMember = (queryParams: Request["query"]) => {
return filterQuery;
};

const addMemberRoles = async (req: Request, res: Response) => {
const { id } = req.params
const callerAddress = (req as any).address
const rolesData = req.body

try {
console.log('roleData: ', JSON.stringify(rolesData))
const team = await prisma.team.findUnique({
where: { id: Number(id) }
})
const ownerAddress = team?.ownerAddress
const userAddress = rolesData.member.address
if (callerAddress !== ownerAddress) {
return errorResponse(403, `Action not authorized`, res)
}
//assign roles to user
for (const role of rolesData.member.roles) {
console.log(`role: `, role)
if (ownerAddress)
await prisma.userRole.create({
data: {
userAddress,
roleId: Number(role.roleId),
assignedBy: ownerAddress
}
})
const _role = await prisma.role.findUnique({
where: { id: Number(role.roleId) }
})
const owner = await prisma.user.findUnique({
where: { address: ownerAddress }
})
//send notification
await addNotification(
[userAddress],
{
message: `You have been assigned a new role: ${_role?.name} by ${owner?.name}`,
subject: `Role Assignment`,
author: `${ownerAddress}` || "",
resource: `role-assignment/${id}`,
}
);
}
//create contract
await prisma.memberTeamsData.create({
data: {
contract: JSON.stringify(rolesData.contract),
ownerSignature: rolesData.signature,
teamId: Number(id),
userAddress: rolesData.member.address
}
})
res.status(201)
.json({
success: true
})
} catch (error) {
return errorResponse(500, error, res)
}
}

const getMemberContract = async (req: Request, res: Response) => {
const { id } = req.params
const memberAddress = req.headers.memberaddress

try {
if (isNaN(Number(id)))
return errorResponse(400, 'Invalid ID Format', res)

if (!memberAddress)
return errorResponse(400, 'No Member Address Supplied', res)

if (typeof memberAddress === "string") {
const contract = await prisma.memberTeamsData.findUnique({
where: {
userAddress_teamId: {
userAddress: memberAddress,
teamId: Number(id)
}
}})

res.status(201)
.json({
success: true,
contract: contract?.contract
})
}
} catch (error) {
return errorResponse(500, error, res)
}
}

const addMemberSignature = async (req: Request, res: Response) => {
const { id, signature } = req.params
const callerAddress = (req as any).address
try {
//console.log(`signature: `, signature)
await prisma.memberTeamsData.update({
where: {
userAddress_teamId: {
userAddress: callerAddress,
teamId: Number(id)
}
},
data: { memberSignature: signature }
})

res.status(201)
.json({
success: true
})
} catch (error) {
return errorResponse(500, error, res)
}
}

export {
addTeam,
updateTeam,
Expand All @@ -398,4 +514,7 @@ export {
getAllTeams,
deleteMember,
addMembers,
addMemberRoles,
getMemberContract,
addMemberSignature
};
Loading