Skip to content

Commit

Permalink
✨ implement user role (#759)
Browse files Browse the repository at this point in the history
* 🗃️ create migration for Role table

* 🗃️ Refactor Role table migration and seed data with UUID primary key

* 🗃️ Add UserRole table with permissions for skillz-admins and world roles

* ✨ Add role management components and update translations

* 🔨 add script to update role in seed
  • Loading branch information
bengeois authored Jan 29, 2025
1 parent fbc4b4e commit 17ff156
Show file tree
Hide file tree
Showing 17 changed files with 409 additions and 1 deletion.
102 changes: 102 additions & 0 deletions hasura/metadata/tables.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,42 @@
- role: skillz-admins
permission:
filter: {}
- table:
name: Role
schema: public
insert_permissions:
- role: skillz-admins
permission:
check: {}
columns:
- name
- id
select_permissions:
- role: skillz-admins
permission:
columns:
- name
- id
filter: {}
- role: world
permission:
columns:
- name
- id
filter: {}
allow_aggregations: true
update_permissions:
- role: skillz-admins
permission:
columns:
- name
- id
filter: {}
check: {}
delete_permissions:
- role: skillz-admins
permission:
filter: {}
- table:
name: Skill
schema: public
Expand Down Expand Up @@ -905,6 +941,72 @@
- role: skillz-admins
permission:
filter: {}
- table:
name: UserRole
schema: public
insert_permissions:
- role: skillz-admins
permission:
check: {}
columns:
- userEmail
- created_at
- roleId
- role: world
permission:
check:
userEmail:
_eq: x-hasura-user-email
columns:
- userEmail
- created_at
- roleId
select_permissions:
- role: skillz-admins
permission:
columns:
- userEmail
- created_at
- roleId
filter: {}
- role: world
permission:
columns:
- userEmail
- created_at
- roleId
filter: {}
allow_aggregations: true
update_permissions:
- role: skillz-admins
permission:
columns:
- userEmail
- created_at
- roleId
filter: {}
check: {}
- role: world
permission:
columns:
- userEmail
- created_at
- roleId
filter:
userEmail:
_eq: x-hasura-user-email
check:
userEmail:
_eq: x-hasura-user-email
delete_permissions:
- role: skillz-admins
permission:
filter: {}
- role: world
permission:
filter:
userEmail:
_eq: x-hasura-user-email
- table:
name: UserSkillDesire
schema: public
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE "public"."Role";
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CREATE TABLE "public"."Role" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" text NOT NULL, PRIMARY KEY ("id") , UNIQUE ("id"), UNIQUE ("name"));COMMENT ON TABLE "public"."Role" IS E'Role for a user';
CREATE EXTENSION IF NOT EXISTS pgcrypto;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE "public"."UserRole";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
CREATE TABLE "public"."UserRole" ("userEmail" text NOT NULL, "roleId" uuid NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), PRIMARY KEY ("userEmail","roleId") , FOREIGN KEY ("userEmail") REFERENCES "public"."User"("email") ON UPDATE restrict ON DELETE cascade, FOREIGN KEY ("roleId") REFERENCES "public"."Role"("id") ON UPDATE restrict ON DELETE cascade, UNIQUE ("userEmail", "roleId"));COMMENT ON TABLE "public"."UserRole" IS E'Relations between user and role';
13 changes: 13 additions & 0 deletions hasura/seeds/10-role.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
INSERT INTO "public"."Role" ("name") VALUES
('Developer / Engineer'),
('Technical Lead'),
('Solution Architect'),
('Technical Expert / Specialist'),
('Product Owner'),
('Product Manager'),
('UX Designer'),
('Engineering Manager'),
('Delivery Manager'),
('Coach (team, organisation)'),
('Consultant')
ON CONFLICT ("name") DO NOTHING;
2 changes: 2 additions & 0 deletions i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
noData: 'No data available.',
requiredField: 'This field is required',
tagRequired: 'Add at least 1 tag',
roleRequired: 'Add at least 1 role',
topicRequired: 'Add at least 1 topic',
duplicatedTag: "This tag already exists, you can't create it",
},
Expand Down Expand Up @@ -240,6 +241,7 @@ export default {
agency: 'Agency',
contact: 'Preferred method of contact',
topics: 'Preferred topics',
roles: 'Roles',
certifications: 'Certifications',
selectPlaceholder: 'Agency',
validFrom: 'valid from',
Expand Down
2 changes: 2 additions & 0 deletions i18n/fr.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export default {
noData: 'Aucune donnée disponible.',
requiredField: 'Ce champs est obligatoire',
tagRequired: 'Ajoutez au minimum 1 tag',
roleRequired: 'Ajoutez au minimum 1 rôle',
topicRequired: 'Ajoutez au minimum 1 sujet',
duplicatedTag: 'Ce tag existe déjà, vous ne pouvez pas le créer',
},
Expand Down Expand Up @@ -246,6 +247,7 @@ export default {
agency: 'Agence',
contact: 'Méthode de contact préférée',
topics: 'Sujets préférés',
roles: 'Rôles',
certifications: 'Certifications',
selectPlaceholder: 'Agence',
validFrom: 'valide depuis',
Expand Down
51 changes: 51 additions & 0 deletions scripts/update-role-referentiel.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import fetch from 'node-fetch'
import fs from 'fs'

/*
* ENVIRONMENT CHECK
*/
if (!process.env.NEXT_PUBLIC_BASE_URL) {
throw new Error(
"ERROR: App couldn't start because NEXT_PUBLIC_BASE_URL isn't defined"
)
}

if (!process.env.NEXT_API_BEARER_TOKEN) {
throw new Error(
"ERROR: App couldn't start because NEXT_API_BEARER_TOKEN isn't defined"
)
}

/*
* GET ALL ROLES FROM API
*/
const response = await fetch(`${process.env.NEXT_PUBLIC_BASE_URL}/api/roles`, {
method: 'GET',
headers: {
Authorization: process.env.NEXT_API_BEARER_TOKEN,
},
})

const { roles: rolesData } = await response.json()

/*
* FORMAT DATA
*/
const roles = rolesData.map((role) => `('${role.name}')`)

/*
* WRITE DATA TO FILE
*/
let writer = fs.createWriteStream('../hasura/seeds/10-roles.sql', {
flags: 'w',
})

writer.write('INSERT INTO "public"."Role" ("name") VALUES\n')

writer.write(roles.join(',\n'))

writer.write('\n ON CONFLICT ("name") DO NOTHING;')

writer.close()

console.log(`Referential Roles successfully updated (${roles.length} roles).`)
39 changes: 39 additions & 0 deletions src/components/atoms/Role.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { RoleItem } from '../../utils/types'

type RoleType = 'common' | 'selected'

type RoleProps = {
type: RoleType
role: RoleItem
keyId: string | number
readOnly?: boolean
callback?: (role: RoleItem) => void
}

export const roleTypeClasses: Record<RoleType, string> = {
common: 'font-bold gradient-red-faded text-light-ultrawhite hover:shadow-xl hover:shadow-light-graybutton hover:dark:shadow-lg hover:dark:shadow-dark-radargrid',
selected:
'text-light-dark text-light-ultrawhite gradient-red hover:drop-shadow-xl hover:dark:shadow-lg hover:dark:shadow-dark-radargrid',
}

export const roleClasses = {
base: 'text-base font-bold py-1 px-5 rounded-full',
disabled: 'disabled:pointer-events-none',
variant: roleTypeClasses,
}

const Role = ({ type, role, keyId, callback, readOnly = false }: RoleProps) => {
return (
<div className="flex-initial py-2" key={`role-${keyId}`}>
<button
className={`${roleClasses.base} ${roleClasses.disabled} ${roleClasses.variant[type]}`}
disabled={readOnly}
onClick={() => callback(role)}
>
<p className="text-sm">{role.name}</p>
</button>
</div>
)
}

export default Role
65 changes: 65 additions & 0 deletions src/components/molecules/Roles.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { RiErrorWarningFill } from 'react-icons/ri'
import { useI18n } from '../../providers/I18nProvider'
import { RoleItem } from '../../utils/types'
import Role from '../atoms/Role'

type RolesProps = {
roles: RoleItem[]
selectedRoles: string[]
title: string
error?: boolean
readOnly?: boolean
addCallback?: (topic: RoleItem) => void
removeCallback?: (topic: RoleItem) => void
}

const Roles = ({
roles,
selectedRoles,
error,
title,
addCallback,
removeCallback,
readOnly = false,
}: RolesProps) => {
const { t } = useI18n()

return (
<div
className={`flex flex-col rounded-lg dark:bg-dark-dark bg-light-dark my-2 p-2`}
>
<div className="flex flex-row">
<p className="text-xl p-2">{title}</p>
{error && (
<div className="flex flex-row items-center">
<RiErrorWarningFill color="#bf1d67" />
<p className="text-light-red pl-1">
{t('error.roleRequired')}
</p>
</div>
)}
</div>
<div className="flex flex-row flex-wrap justify-around px-4">
{roles.map((role, key) => {
const selected = selectedRoles.some((t) => role.id === t)
return (
<Role
role={role}
key={key}
keyId={key}
type={selected ? 'selected' : 'common'}
callback={
selected
? () => removeCallback(role)
: () => addCallback(role)
}
readOnly={readOnly}
/>
)
})}
</div>
</div>
)
}

export default Roles
Loading

0 comments on commit 17ff156

Please sign in to comment.