Compare commits

..

3 Commits

Author SHA1 Message Date
Lebaud Antoine 9eb4a1e9e3 🔧(backend) udpate email invitation sender
Replace the placeholder setting by a randomly picked 'do not reply'
email adress. Might be improved later on.
2024-04-02 23:26:38 +02:00
Lebaud Antoine d8b6bcc9eb 📱(frontend) handle extremely long name
Currently, with a very long group name (~100 char), text was overflowing
the flex elements.

I reworked the current layout to prevent this issue in Team info and
Team pannel.

I needed to debug some buggy flex layout issues. I decided to refactor
some styling, and ended up doing a big refactoring.

Working with Box and Text components cause high friction when
manipulating them. They use mix styling approaches ... which is quite
misleading for any developper joining the project.

It's mainly due to the fact that creating such generic wrappers,
relying on (heavy) interpolation functions might be against
styled component spirit.

I love manipulating inline styling, but styled component might not
be the best tool to take this approach.

I tried to align main screen with Johann's design to be pixel-perfect.
However, the actual design is not appropriate for responsive screen.
2024-04-02 23:21:44 +02:00
Lebaud Antoine 418bed945a 🩹(frontend) display email or name in delete modal
User's name could be null, leading to an empty text box when opening the
delete modal.

Prevent it by displaying user's name or email when deleting an user.
2024-04-02 22:53:43 +02:00
88 changed files with 712 additions and 2141 deletions
+11 -11
View File
@@ -16,7 +16,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: show
@@ -39,7 +39,7 @@ jobs:
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Check that the CHANGELOG has been modified in the current branch
@@ -49,7 +49,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
@@ -200,7 +200,7 @@ jobs:
- name: Run e2e tests
run: cd src/frontend/ && yarn e2e:test
- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
@@ -214,7 +214,7 @@ jobs:
working-directory: src/mail
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Node.js
uses: actions/setup-node@v4
with:
@@ -238,9 +238,9 @@ jobs:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: Install development dependencies
@@ -284,7 +284,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Create writable /data
run: |
sudo mkdir -p /data/media && \
@@ -295,7 +295,7 @@ jobs:
name: mails-templates
path: src/backend/core/templates/mail
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: '3.10'
- name: Install development dependencies
@@ -313,7 +313,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v2
- name: Install gettext (required to make messages)
run: |
@@ -321,7 +321,7 @@ jobs:
sudo apt-get install -y gettext
- name: Install Python
uses: actions/setup-python@v5
uses: actions/setup-python@v3
with:
python-version: '3.10'
+17 -17
View File
@@ -1,24 +1,24 @@
SOPS_PRIVATE=ENC[AES256_GCM,data:53ysyQ9gq2PnAQKNjOL+e+Bu5SQIuOguz8Bo5CpqbpYsF0AmV1WsOutckdClbu6ApqV3m9/Cj1FJ30+L/+j05pvcpqMeehPQwGQ=,iv:VMuML9IXiEqKY9jp+ny76jnQHmewq2rqdBy1wYpZkSI=,tag:aAZgwiWDg1AG4wk3f2Fq4w==,type:str]
CROWDIN_API_TOKEN=ENC[AES256_GCM,data:bwh38oLDH4BpI2H+7oUjtVizyrYvVJ6Av4ECTnyPPthMz6DCaYQn55RXp8rQDgJj4bPRls+JcRVC94zYIjgpkDsbbcqHr620KQKHQHMgoOQ=,iv:hydpwWtCiOkhBpAYyNwDzSjhjfdUJcKX7YX3/PXteN0=,tag:eQLniL5XxkNs5yThUuQHyw==,type:str]
CROWDIN_BASE_PATH=ENC[AES256_GCM,data:LJZE454A6qg=,iv:yIjGACBJSX3S9g7PAHRFn074xL94fHvMLcTKzFYwkwo=,tag:1Z8+UbeDOvTxR80b95KumQ==,type:str]
CROWDIN_PROJECT_ID=ENC[AES256_GCM,data:THoNz661,iv:Ixd0D9tnpEWd2yqZui1HJQEO/h7YsAC1R9Vjj8OHBjA=,tag:wfDHhzaXLD3NwY5zDj24RA==,type:str]
DOCKER_HUB_PASSWORD=ENC[AES256_GCM,data:jj92OOVMtsagOXQ=,iv:r/u8M70PspZMFCbi8a3FvuCDtWt+9YGArPNHZRpHA+k=,tag:WM3vzVkuQZVdHa3wh4satg==,type:str]
DOCKER_HUB_USER=ENC[AES256_GCM,data:btdtLdLApQ==,iv:y1o2zwyzusBS6JiQSEtZwS2zctISo+UgAFhyZ53vbKQ=,tag:ZLkMJydgjMBmbbKq979z7g==,type:str]
ARGOCD_WEBHOOK_URL=ENC[AES256_GCM,data:0TnoZv7vQI+8MZ/7EITx0Mvez66G6BcCzw+Mic+NH2qh0BdZBH8ynkYBleKw9V6TbucgHasa7duL,iv:GeE5tSpjAndThrXrzz8Dk6ah9Bxv6JQCJmKAfsToDi0=,tag:O2pIhA0ge1xygIv0izSMxg==,type:str]
ARGOCD_WEBHOOK_SECRET=ENC[AES256_GCM,data:SrdWdV24lGztyUnFXeOYGAhqTErRFakIm7hBw8n4NKW6ll6AgeZKY6w7pbvgFknQ+NlRd/EK7bYk7CZtPDGU6zM=,iv:IkWxnTWrvzWwNh4RSt3N7iPHA7K7jkzSHa4CHptxxvU=,tag:XFVYBRsuDF/La1/8ADQ2jw==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5KzVTKzFmN0Q3Umk3RGNp\nSmhrUklSZnFaVWZiWXRJR0xTVlRrL3RsMUE0CklmZFVveGRzWVNaRnFGRS9XaHhk\nbnlQYjM3dDJuRlVVZkppTlZ1RjJqS1UKLS0tIHBoamFOeDVVdmhDcFN1a3FidFlO\nN2JKTlpLNXo0SHJudWZpUWJpcE5sOWcKkfq/oWHCyy4jz6NkOUdCCDVtHV+hw7Dz\nqtc52m7dvEk1E6seD/zbes2BMQo5t0FybzikMke9cASe13cdYMGmiA==\n-----END AGE ENCRYPTED FILE-----\n
SOPS_PRIVATE=ENC[AES256_GCM,data:FAgeMw3bbPVNZEuyuJ4rMeknOH4UZ1vC7k3S0qe6aaeGG3HHfEIpWHckCjHLDTz6hNDUxWSr/WPytntbGjqcvzKwYZBeehGx9hY=,iv:O64EPSvyYSxjHOIIrdjMQYIJR9HW2CxE+6Ay0e0dvoc=,tag:64G0A4kIJ8AzNx3+CluW5Q==,type:str]
CROWDIN_API_TOKEN=ENC[AES256_GCM,data:IlmqbPlM6s9paCAxn40v4LNEGbzYlm9SVplXfP8PI8Ei/K5oGtLPquoDokK33ySEfEh5b5xLAZjlmDyICdv89ETdd7RLB/63f2LduMn8n0A=,iv:gdcCrv58wkLC6wufFvHGrHB/A6aLXAApJ036BYjlN8g=,tag:wReGIj4izqzBHY7X0rqOAg==,type:str]
CROWDIN_BASE_PATH=ENC[AES256_GCM,data:bepqdi4eaMM=,iv:nF7FSUiuRu4kTYeEQB3cN/Fwrpt0fB0dC/dm4poec7w=,tag:kbxhwe1SQYkaddKyY1UQDg==,type:str]
CROWDIN_PROJECT_ID=ENC[AES256_GCM,data:z8ZkbbR9,iv:Uc2aGQ9dAkiUcTpBqcIqD2fXt4+/ObJJz/3D8BhN9/A=,tag:ZDqDpsD7IN7iyFvKmxoI2w==,type:str]
DOCKER_HUB_PASSWORD=ENC[AES256_GCM,data:3E/4G2zOBNZqkGc=,iv:zbQzrgLYgNp99zRUZ3ICG47OkLPV1HnhcNoIu3cSmpc=,tag:CEvKG2HVd2ijjGfzkM005Q==,type:str]
DOCKER_HUB_USER=ENC[AES256_GCM,data:Qc/HR/O8nw==,iv:nvXUmLHgNFQwm6aIpBcPvOdITOkw1YT7hIyk4ptuOOw=,tag:qoqSA9NlGCbor+7OyGArfw==,type:str]
ARGOCD_WEBHOOK_URL=ENC[AES256_GCM,data:WhW6TQQV/IQteW/jQukjlTaTLeNUzEzadp+HJEC7bqrWR88ZBUlk8sfF5KV7PAAAZtvMhe4mi/yl,iv:CInpRuJpKFpxozdzHQkfDxbhQYyy+VByzrDr2ZSmJ7E=,tag:RNmIoYtA5DSi1W7TDsJHxw==,type:str]
ARGOCD_WEBHOOK_SECRET=ENC[AES256_GCM,data:KTT5ubYdm5oVzXL0sM9HdNObVKhSD1spB3KYgM6CskEpQutb3R4Lu5mImft9C9tRKIkZetxV9YyLawCPaZm5P1U=,iv:Xp+xE43a6/GODhhujnSrTfcQJ2P9u4YrRakmxLwEtEc=,tag:jYAk+4X7x44edb0vcbnKWA==,type:str]
sops_age__list_0__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBHaHpGT0lLR21qRi8wZCt2\nc0t5NGlDR29lK2JscE4rc3VZOU5ic3FBdkhnCnJYdDdvMlBROWhPc2pjbDJTbmo0\ncENxV0hDYzZKSFh5RTNnUTRyaUdGLzgKLS0tIENIeDQyQ1NzMkF6cTZVRmU1VC9D\nNy9BTElMQlBEUWhCQWxpdVI1enJNblEKbGEkN7JPlj+OqBHYhZhZwfjpdxGlkjti\nBPsDHIklHYufk9+TjnxBYL1Oe6zLn4zlG0r/w8AHrTo6oke5dUjvKg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_0__map_recipient=age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGRS9pcEprcHlJSHJaWWlQ\nZ0ZTbzhoUXYxUXlNdDVyQXdvQUhVcjFya0NvCk1UOFNSUXRsMTl3Zy9ibmhMWnlN\nSUVCVW9NaDAwYk0wdlRvREZDSHI5cU0KLS0tIDhhRnNoS0xoNjE0d3FKaXZhN1V2\nbW5KOHFGcGpiOUxYWUVxZWFCejJ1SW8KNY6H8A3DNhJ2tqy13md0icS6fzHmd8cH\nah6FyrgxXa6zJbmC/bKRVPiAU7D2xAgkyqS4nzvXjeAaEMef+a97GQ==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_1__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArOG1XL1NFc29xVWFFdW9Q\nM0FrY3BEazJRRXNHMjVpS2hCVVBNSllUU1dJCkRIT00xSVAzeEozZndwTzlmY1Bn\nc3VJYmQrVzRXZTF1TjR4QXQrYXpIaG8KLS0tIEtXNTkvb2tRcjR0YWdneTI2dlBu\nY2tkYVU4UGtZdEIxQWMxQ1h6aHQ1RjQKpPQRQZtMDMkmSUhm7PQXpBHYK45Fm7Gx\nNL4gpLq1bJ6tNlnvP2zj2OOJ7NsiSeH8Vey6NON6E5wDKckBkpZUdg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_1__map_recipient=age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBKTVNHS3ZpTS9HVEV6Z3F0\ncWRxd1lsME1oUllDQjNoczN1TWhBY09DZkFvCm9tNzVOQ3Nxc3lCRk9WYXp1VmJB\nZWt4SlFlNTQxdWJGVTVEYTNablVNMFUKLS0tIEM3clpvUmVYRUJzbis1ZHpHcDAv\nY3d0VUFyUFFQc1lYbnVkRklKVGQ5MUEKy6lJML7AgC7BLYTEVJz9bnNIEXxjKNps\nV2IQWMorUsAC9fg0tPNDbAUDfgP4jkPxNEMa10vGcjwRKcKUazXj+A==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_2__map_recipient=age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrOVdXOFZiQTlNZ1IwYU9u\nSzkyNk93RkIvLzJ5NjZHcmlDS1B5TUhraVJBClpXdGtqK2pxWnh1YXNDQ1Ftb2lz\nWmd3QmxpLytMMjhLR1U5Y3kwZ3krN0kKLS0tIFl4SVFjb1JPcGZ4SjNINkhqS1Uy\nTFI0dThVdFJpc0lydjR1STVRRXB2TUkKj/0oPq3pUXLY2LUNjUsrNekNorB83ejs\nBCIpaZzx2FRNHiwiOq+3m7FcX7VZoj47kqmiNbs6uyrXtv0gNGbzVA==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_2__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBUUEM5ZzNST2RHTzRKUUxH\nbGxsOVoxTkVQWnFBa3lweExjSG5tZW02ZTBVCmhmLzRkbEVUUFNOZXdZRG5QUUJL\nL0lvMDArRHBGbmsrR1Y0NE9DcmtwbWcKLS0tIDljdmdGaTVVdnBxdjlYM0IrWTZ1\nYU03M1dHZm5nMDY0ZE9TSzl2Mk9YcG8KZ6vkG1KUIpS3BnIWcluA0nc5YX5AB3RH\nPJqFTdNwUfeB8omOyifF4jxU3Jqm53d3kVmiSPXTlaOYwqrhbT1oAw==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_2__map_recipient=age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
sops_age__list_3__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB0c2x0YVB4cW1jVFRZVlBP\nNFJiSHIrWk9HWGgreVRSZWhkd3cveDBDZ1RZCkQzYnFxMEhUb2s2U3drZzQwTW5D\nbU9iL0RoN2Ztb0M2UEluOUQ5M3RtMEEKLS0tIEhJRnZGekxWR3dRejV1SkNDQkda\nQkY5MGZyZW1QNzdveXFTbGd0WjRZaTAKbv+2BPThEjNn7yjK4aX4C7kvnXQgf1Qb\n6J72BKD8AtHOJVdp/d1+I41ejwXdfBbSKN5O6rnPozH60GDWFlQRwA==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_3__map_recipient=age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMEdlbi81UXVCZ1o2RjJu\nbUNkdXIxcTJoU004MUxtZWE5dWtHUm01ZmpVCm9RRzNkVWhsakFRTGRBbEdMQzhw\ncU54ekphSENPNlFsOXp1blQ3SmlaelkKLS0tIE9pTW5uQ2JrbUFQcGMwL1lqL000\nekdNa2pQWHA1cXBaTGxZT1htNjhzcmcKGD1xp6Wc2CYzmkI2blmX4xt8It5HPX/w\nj5oynxnDwwPkknRdZ1bDlre5fXTzKGd917RcU2WUs9q9cbZHTmTQQQ==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_4__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4bzBsVUdlV1dzNVZoWGs2\nR3k0ZGh4cTlNTWdqNXlzc29KeTNTcmd6MHlZCi9wa29CNzcwWmtyT2U2bVArQXlU\nOGJXY3VaT2J5SEd1clY2WFVLa0s2a0EKLS0tIGlYZzRFT29ld2t6SU5tUUhIeUpG\nRnQwcTN0OFZJNGJicUNmeVE0eVcrU3MK96xiB4AkQSXLnP5XvVlQhvjQEQKhTZuO\nYbO1uP0EGSQ+f6uP3ENw4BFAgdB3y0YFcVm5VtTxN4Tpw3ZUVWtqew==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_4__map_recipient=age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhVEVMcU9nYUhPMTU4d2Jj\nclR1bUR0QlZEdFcraDZ6ODhwcTJsZ1BCNUR3Ci9MamhHYXN2dU9nRjNkbnUzQ1U3\nMjRjVG5kU3JFSmJmQ29UV20wSmVHbk0KLS0tIGp0aVBlNzI1V3lGYy9ONzhlVUR0\neTlPN2drTTAwOFlCUjA2U0FCY1lFVHMKzolngi5XjFQKUnwLpdpqmBDPuY0Bsurk\nyvKE/Lou9Pcm7OhZePTwoVIcBtS313vzh8xmnVeuJPpIzLjkfvJwRg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_5__map_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBoWkEzQlRLNVd3U3hrU2dh\nY0l1OGVlYjJNZEIzK1R5NWFCK1lnK2pMVnlBCksrR1JKZTFmdzJCekZkQStvejBL\nd05wejByZjdnN2xweVhBemNiSDUwUEUKLS0tIEJIVGYzbWNBN2hrNEtoY2xMSk9v\naWw1THRBbmpDbGc2eE1WV0VSVUtoNm8KyCP6BtJRNUfvlAiuzQ1xhpjm6YFUA66G\n/qk9WLm07qHfFD5r2dGQBksN61Nk+akF0BGa56qnAEJp63lP8u1xbg==\n-----END AGE ENCRYPTED FILE-----\n
sops_age__list_5__map_recipient=age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
sops_lastmodified=2024-04-03T15:36:15Z
sops_mac=ENC[AES256_GCM,data:1v44C4K4YjV1m7tZKRgj8SiDamdD+L4p3TVwwOl6+05KCOh2uH2ohH+5MH7MTFL489oqaadpjBQfELSJ8h/4fN5MT6+Trbtk5QFLv4moLZx1tSCE1Tuam2cicFem2mlOrxb0pK/tU1qzCLvZke3yvFmiJEa+92u7y96hXM4VR6Y=,iv:23T3Tl5DvRH8zvef7ftbr5GWk+YFfLCzZ/eEzqjMKXY=,tag:TIch+2911w5qleXo55zM0w==,type:str]
sops_lastmodified=2024-03-26T10:02:48Z
sops_mac=ENC[AES256_GCM,data:p7Br4MRgpr26vSS79QBwYRXQWdb20lbrSxOdcrj385EhDVaqZM9aaycf09FItuBrQDuNO3NVSoGnJvWhekwRXXJlxXDENmjP9YBnNt25WXCa2+PLQGzvGbZjo/KHky5zqwVV5XaJ++UuO7VJZgB6m5m7RPZEBCo4cwy7rvNeyEs=,iv:XaavC9VEDckO1QpsPjUwf5BpJzFcEdxycSZvuFX8D7A=,tag:S82ex6+X4MUkkR8m9UAgqA==,type:str]
sops_unencrypted_suffix=_unencrypted
sops_version=3.8.1
+2 -2
View File
@@ -2,14 +2,14 @@ creation_rules:
# Here we have
# - Jacques key-id: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
# - github-repo key-id: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
# - Anthony Le-Courric key-id: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
# - Anthony Le-Courric key-id: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
# - Antoine Lebaud key-id: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
# - Marie Pupo Jeammet key-id: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
# - argocd key-id: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
- age:
age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x,
age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7,
age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg,
age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv,
age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3,
age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa,
age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
+1 -1
View File
@@ -183,7 +183,7 @@ migrate: ## run django migrations for the people project.
superuser: ## Create an admin superuser with password "admin"
@echo "$(BOLD)Creating a Django superuser$(RESET)"
@$(MANAGE) createsuperuser --admin_email admin@example.com --password admin
@$(MANAGE) createsuperuser --email admin@example.com --password admin
.PHONY: superuser
back-i18n-compile: ## compile the gettext files
-11
View File
@@ -40,10 +40,6 @@ services:
- postgresql
- mailcatcher
- redis
# FIXME: temporary configuration to connect AC and desk
networks:
- default
- dataprovider_outside
celery-dev:
user: ${DOCKER_USER:-1000}
@@ -173,10 +169,3 @@ services:
- "8080:8080"
depends_on:
- kc_postgresql
# FIXME: temporary configuration to connect AC and desk
networks:
dataprovider_outside:
external: true
driver: bridge
name: "${DESK_NETWORK:-fc_public}"
-33
View File
@@ -33,36 +33,3 @@ LOGOUT_REDIRECT_URL=http://localhost:3000
OIDC_REDIRECT_ALLOWED_HOSTS=["http://localhost:8083", "http://localhost:3000"]
OIDC_AUTH_REQUEST_EXTRA_PARAMS={"acr_values": "eidas1"}
OIDC_RS_CLIENT_ID=people
OIDC_RS_CLIENT_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RS_AUTH_SECRET=ThisIsAnExampleKeyForDevPurposeOnly
OIDC_RS_PRIVATE_KEY_STR="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3boG1kwEGUYL+
U58RPrVToIsF9jHB64S6WJIIInPmAclBciXFb6BWG11mbRIgo8ha3WVnC/tGHbXb
ndiKdrH2vKHOsDhV9AmgHgNgWaUK9L0uuKEb/xMLePYWsYlgzcQJx8RZY7RQyWqE
20WfzFxeuCE7QMb6VXSOgwQMnJsKocguIh3VCI9RIBq3B1kdgW35AD63YKOygmGx
qjcWwbjhKLvkF7LpBdlyAEzOKqg4T5uCcHMfksMW2+foTJx70RrZM/KHU+Zysuw7
uhhVsgPBG+CsqBSjHQhs7jzymqxtQAfe1FkrCRxOq5Pv2Efr7kgtVSkJJiX3KutM
vnWuEypxAgMBAAECggEAGqKS9pbrN+vnmb7yMsqYgVVnQn0aggZNHlLkl4ZLLnuV
aemlhur7zO0JzajqUC+AFQOfaQxiFu8S/FoJ+qccFdATrcPEVmTKbgPVqSyzLKlX
fByGll5eOVT95NMwN8yBGgt2HSW/ZditXS/KxxahVgamGqjAC9MTSutGz/8Ae1U+
DNDBJCc6RAqu3T02tV9A2pSpVC1rSktDMpLUTscnsfxpaEQATd9DJUcHEvIwoX8q
GJpycPEhNhdPXqpln5SoMHcf/zS5ssF/Mce0lJJXYyE0LnEk9X12jMWyBqmLqXUY
cKLyynaFbis0DpQppwKx2y8GpL76k+Ci4dOHIvFknQKBgQDj/2WRMcWOvfBrggzj
FHpcme2gSo5A5c0CVyI+Xkf1Zab6UR6T7GiImEoj9tq0+o2WEix9rwoypgMBq8rz
/rrJAPSZjgv6z71k4EnO2FIB5R03vQmoBRCN8VlgvLM0xv52zyjV4Wx66Q4MDjyH
EgkpHyB0FzRZh0UzhnE/pYSetQKBgQDN9eLB1nA4CBSr1vMGNfQyfBQl3vpO9EP4
VSS3KnUqCIjJeLu682Ylu7SFxcJAfzUpy5S43hEvcuJsagsVKfmCAGcYZs9/xq3I
vzYyhaEOS5ezNxLSh4+yCNBPlmrmDyoazag0t8H8YQFBN6BVcxbATHqdWGUhIhYN
eEpEMOh2TQKBgGBr7kRNTENlyHtu8IxIaMcowfn8DdUcWmsW9oBx1vTNHKTYEZp1
bG/4F8LF7xCCtcY1wWMV17Y7xyG5yYcOv2eqY8dc72wO1wYGZLB5g5URlB2ycJcC
LVIaM7ZZl2BGl+8fBSIOx5XjYfFvQ+HLmtwtMchm19jVAEseHF7SXRfRAoGAK15j
aT2mU6Yf9C9G7T/fM+I8u9zACHAW/+ut14PxN/CkHQh3P16RW9CyqpiB1uLyZuKf
Zm4cYElotDuAKey0xVMgYlsDxnwni+X3m5vX1hLE1s/5/qrc7zg75QZfbCI1U3+K
s88d4e7rPLhh4pxhZgy0pP1ADkIHMr7ppIJH8OECgYEApNfbgsJVPAMzucUhJoJZ
OmZHbyCtJvs4b+zxnmhmSbopifNCgS4zjXH9qC7tsUph1WE6L2KXvtApHGD5H4GQ
IH5em4M/pHIcsqCi1qggBMbdvzHBUtC3R4sK0CpEFHlN+Y59aGazidcN2FPupNJv
MbyqKyC6DAzv4jEEhHaN7oY=
-----END PRIVATE KEY-----
"
+5 -14
View File
@@ -53,15 +53,6 @@ class TeamAccessInline(admin.TabularInline):
readonly_fields = ("created_at", "updated_at")
class TeamWebhookInline(admin.TabularInline):
"""Inline admin class for team webhooks."""
extra = 0
autocomplete_fields = ["team"]
model = models.TeamWebhook
readonly_fields = ("created_at", "updated_at")
@admin.register(models.User)
class UserAdmin(auth_admin.UserAdmin):
"""Admin class for the User model"""
@@ -76,7 +67,7 @@ class UserAdmin(auth_admin.UserAdmin):
)
},
),
(_("Personal info"), {"fields": ("admin_email", "language", "timezone")}),
(_("Personal info"), {"fields": ("email", "language", "timezone")}),
(
_("Permissions"),
{
@@ -97,13 +88,13 @@ class UserAdmin(auth_admin.UserAdmin):
None,
{
"classes": ("wide",),
"fields": ("admin_email", "password1", "password2"),
"fields": ("email", "password1", "password2"),
},
),
)
inlines = (IdentityInline, TeamAccessInline)
list_display = (
"admin_email",
"email",
"created_at",
"updated_at",
"is_active",
@@ -114,14 +105,14 @@ class UserAdmin(auth_admin.UserAdmin):
list_filter = ("is_staff", "is_superuser", "is_device", "is_active")
ordering = ("is_active", "-is_superuser", "-is_staff", "-is_device", "-updated_at")
readonly_fields = ("id", "created_at", "updated_at")
search_fields = ("id", "admin_email", "identities__sub", "identities__email")
search_fields = ("id", "email", "identities__sub", "identities__email")
@admin.register(models.Team)
class TeamAdmin(admin.ModelAdmin):
"""Team admin interface declaration."""
inlines = (TeamAccessInline, TeamWebhookInline)
inlines = (TeamAccessInline,)
list_display = (
"name",
"slug",
+23 -3
View File
@@ -52,22 +52,42 @@ class UserSerializer(DynamicFieldsModelSerializer):
"""Serialize users."""
timezone = TimeZoneSerializerField(use_pytz=False, required=True)
email = serializers.ReadOnlyField()
name = serializers.ReadOnlyField()
name = serializers.SerializerMethodField(read_only=True)
email = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.User
fields = [
"id",
"name",
"email",
"language",
"name",
"timezone",
"is_device",
"is_staff",
]
read_only_fields = ["id", "name", "email", "is_device", "is_staff"]
def _get_main_identity_attr(self, obj, attribute_name):
"""Return the specified attribute of the main identity."""
try:
return getattr(obj.main_identity[0], attribute_name)
except TypeError:
return getattr(obj.main_identity, attribute_name)
except IndexError:
main_identity = obj.identities.filter(is_main=True).first()
return getattr(obj.main_identity, attribute_name) if main_identity else None
except AttributeError:
return None
def get_name(self, obj):
"""Return main identity's name."""
return self._get_main_identity_attr(obj, "name")
def get_email(self, obj):
"""Return main identity's email."""
return self._get_main_identity_attr(obj, "email")
class TeamAccessSerializer(serializers.ModelSerializer):
"""Serialize team accesses."""
+5 -2
View File
@@ -202,7 +202,7 @@ class UserViewSet(
Prefetch(
"identities",
queryset=models.Identity.objects.filter(is_main=True),
to_attr="_identities_main",
to_attr="main_identity",
)
)
@@ -245,6 +245,9 @@ class UserViewSet(
Return information on currently logged user
"""
user = request.user
user.main_identity = models.Identity.objects.filter(
user=user, is_main=True
).first()
return response.Response(
self.serializer_class(user, context={"request": request}).data
)
@@ -375,7 +378,7 @@ class TeamAccessViewSet(
Prefetch(
"user__identities",
queryset=models.Identity.objects.filter(is_main=True),
to_attr="_identities_main",
to_attr="main_identity",
)
)
# Abilities are computed based on logged-in user's role and
-9
View File
@@ -3,7 +3,6 @@ Core application enums declaration
"""
from django.conf import global_settings, settings
from django.db import models
from django.utils.translation import gettext_lazy as _
# Django sets `LANGUAGES` by default with all supported languages. We can use it for
@@ -15,11 +14,3 @@ ALL_LANGUAGES = getattr(
"ALL_LANGUAGES",
[(language, _(name)) for language, name in global_settings.LANGUAGES],
)
class WebhookStatusChoices(models.TextChoices):
"""Defines the possible statuses in which a webhook can be."""
FAILURE = "failure", _("Failure")
PENDING = "pending", _("Pending")
SUCCESS = "success", _("Success")
+3 -13
View File
@@ -120,13 +120,13 @@ class ContactFactory(BaseContactFactory):
class UserFactory(factory.django.DjangoModelFactory):
"""A factory to create random users for testing purposes."""
"""A factory to random users for testing purposes."""
class Meta:
model = models.User
django_get_or_create = ("admin_email",)
django_get_or_create = ("email",)
admin_email = factory.Faker("email")
email = factory.Faker("email")
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
password = make_password("password")
@@ -177,16 +177,6 @@ class TeamAccessFactory(factory.django.DjangoModelFactory):
role = factory.fuzzy.FuzzyChoice([r[0] for r in models.RoleChoices.choices])
class TeamWebhookFactory(factory.django.DjangoModelFactory):
"""Create fake team webhooks for testing."""
class Meta:
model = models.TeamWebhook
team = factory.SubFactory(TeamFactory)
url = factory.Sequence(lambda n: f"https://example.com/Groups/{n!s}")
class InvitationFactory(factory.django.DjangoModelFactory):
"""A factory to create invitations for a user"""
+2 -19
View File
@@ -1,4 +1,4 @@
# Generated by Django 5.0.3 on 2024-03-25 22:58
# Generated by Django 5.0.2 on 2024-03-05 17:09
import django.contrib.auth.models
import django.core.validators
@@ -45,7 +45,7 @@ class Migration(migrations.Migration):
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin email address')),
('email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='email address')),
('language', models.CharField(choices="(('en-us', 'English'), ('fr-fr', 'French'))", default='en-us', help_text='The language in which the user wants to see the interface.', max_length=10, verbose_name='language')),
('timezone', timezone_field.fields.TimeZoneField(choices_display='WITH_GMT_OFFSET', default='UTC', help_text='The timezone in which the user wants to see times.', use_pytz=False)),
('is_device', models.BooleanField(default=False, help_text='Whether the user is a device or a real user.', verbose_name='device')),
@@ -144,23 +144,6 @@ class Migration(migrations.Migration):
name='users',
field=models.ManyToManyField(related_name='teams', through='core.TeamAccess', to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name='TeamWebhook',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, help_text='primary key for the record as UUID', primary_key=True, serialize=False, verbose_name='id')),
('created_at', models.DateTimeField(auto_now_add=True, help_text='date and time at which a record was created', verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, help_text='date and time at which a record was last updated', verbose_name='updated at')),
('url', models.URLField(verbose_name='url')),
('secret', models.CharField(blank=True, max_length=255, null=True, verbose_name='secret')),
('status', models.CharField(choices=[('failure', 'Failure'), ('pending', 'Pending'), ('success', 'Success')], default='pending', max_length=10)),
('team', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='webhooks', to='core.team')),
],
options={
'verbose_name': 'Team webhook',
'verbose_name_plural': 'Team webhooks',
'db_table': 'people_team_webhook',
},
),
migrations.AddConstraint(
model_name='contact',
constraint=models.CheckConstraint(check=models.Q(('base__isnull', False), ('owner__isnull', True), _negated=True), name='base_owner_constraint', violation_error_message='A contact overriding a base contact must be owned.'),
+8 -94
View File
@@ -13,7 +13,7 @@ from django.conf import settings
from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.core import exceptions, mail, validators
from django.db import models, transaction
from django.db import models
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.functional import lazy
@@ -24,9 +24,6 @@ from django.utils.translation import override
import jsonschema
from timezone_field import TimeZoneField
from core.enums import WebhookStatusChoices
from core.utils.webhooks import scim_synchronizer
logger = getLogger(__name__)
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -77,7 +74,7 @@ class BaseModel(models.Model):
def save(self, *args, **kwargs):
"""Call `full_clean` before saving."""
self.full_clean()
return super().save(*args, **kwargs)
super().save(*args, **kwargs)
class Contact(BaseModel):
@@ -160,9 +157,7 @@ class Contact(BaseModel):
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
"""User model to work with OIDC only authentication."""
admin_email = models.EmailField(
_("admin email address"), unique=True, null=True, blank=True
)
email = models.EmailField(_("email address"), unique=True, null=True, blank=True)
profile_contact = models.OneToOneField(
Contact,
on_delete=models.SET_NULL,
@@ -204,7 +199,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
objects = auth_models.UserManager()
USERNAME_FIELD = "admin_email"
USERNAME_FIELD = "email"
REQUIRED_FIELDS = []
class Meta:
@@ -216,32 +211,9 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
return (
str(self.profile_contact)
if self.profile_contact
else self.admin_email or str(self.id)
else self.email or str(self.id)
)
def _get_identities_main(self):
"""Return a list with the main identity or an empty list."""
try:
return self._identities_main
except AttributeError:
return self.identities.filter(is_main=True)
@property
def name(self):
"""Return main identity's name."""
try:
return self._get_identities_main()[0].name
except IndexError:
return None
@property
def email(self):
"""Return main identity's email."""
try:
return self._get_identities_main()[0].email
except IndexError:
return None
def clean(self):
"""Validate fields."""
super().clean()
@@ -253,10 +225,8 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
def email_user(self, subject, message, from_email=None, **kwargs):
"""Email this user."""
email = self.email or self.admin_email
if not email:
raise ValueError("You must first set an email for the user.")
mail.send_mail(subject, message, from_email, [email], **kwargs)
main_identity = self.identities.get(is_main=True)
mail.send_mail(subject, message, from_email, [main_identity.email], **kwargs)
@classmethod
def get_email_field_name(cls):
@@ -398,7 +368,7 @@ class Team(BaseModel):
return self.name
def save(self, *args, **kwargs):
"""Override save function to compute the slug."""
"""Overriding save function to compute the slug."""
self.slug = self.get_slug()
return super().save(*args, **kwargs)
@@ -465,34 +435,6 @@ class TeamAccess(BaseModel):
def __str__(self):
return f"{self.user!s} is {self.role:s} in team {self.team!s}"
def save(self, *args, **kwargs):
"""
Override save function to fire webhooks on any addition or update
to a team access.
"""
if self._state.adding:
self.team.webhooks.update(status=WebhookStatusChoices.PENDING)
with transaction.atomic():
instance = super().save(*args, **kwargs)
scim_synchronizer.add_user_to_group(self.team, self.user)
else:
instance = super().save(*args, **kwargs)
return instance
def delete(self, *args, **kwargs):
"""
Override delete method to fire webhooks on to team accesses.
Don't allow deleting a team access until it is successfully synchronized with all
its webhooks.
"""
self.team.webhooks.update(status=WebhookStatusChoices.PENDING)
with transaction.atomic():
arguments = self.team, self.user
super().delete(*args, **kwargs)
scim_synchronizer.remove_user_from_group(*arguments)
def get_abilities(self, user):
"""
Compute and return abilities for a given user taking into account
@@ -543,34 +485,6 @@ class TeamAccess(BaseModel):
}
class TeamWebhook(BaseModel):
"""Webhooks fired on changes in teams."""
team = models.ForeignKey(Team, related_name="webhooks", on_delete=models.CASCADE)
url = models.URLField(_("url"))
secret = models.CharField(_("secret"), max_length=255, null=True, blank=True)
status = models.CharField(
max_length=10,
default=WebhookStatusChoices.PENDING,
choices=WebhookStatusChoices.choices,
)
class Meta:
db_table = "people_team_webhook"
verbose_name = _("Team webhook")
verbose_name_plural = _("Team webhooks")
def __str__(self):
return f"Webhook to {self.url} for {self.team}"
def get_headers(self):
"""Build header dict from webhook object."""
headers = {"Content-Type": "application/json"}
if self.secret:
headers["Authorization"] = f"Bearer {self.secret:s}"
return headers
class Invitation(BaseModel):
"""User invitation to teams."""
-118
View File
@@ -1,118 +0,0 @@
"""Resource Server authentication class"""
from base64 import b64decode
from requests.exceptions import HTTPError
from rest_framework.authentication import BaseAuthentication, get_authorization_header
from rest_framework.exceptions import AuthenticationFailed
from ..models import User
from .clients import AuthorizationServerClient, ResourceServerClient
from .utils import get_settings
class ResourceServerAuthentication(BaseAuthentication):
"""Token-based authentication for Resource Server (RS).
Authenticate by passing the token received from the OIDC Provider (OP).
The Resource Server will introspect the token, while the OIDC Provider validates
its integrity and permissions.
"""
@staticmethod
def _decode_authorization_header(request):
"""Get token and received_secret passed by the Service Provider (SP)."""
authorization_header = get_authorization_header(request).split()
if (
not authorization_header
or authorization_header[0].lower() != "Bearer".lower().encode()
):
msg = "Invalid token header. No credentials provided."
raise AuthenticationFailed(msg)
if len(authorization_header) == 1:
msg = "Invalid token header. No credentials provided."
raise AuthenticationFailed(msg)
if len(authorization_header) > 2:
msg = "Invalid token header. Token string should not contain spaces."
raise AuthenticationFailed(msg)
decoded_bearer = b64decode(authorization_header[1]).decode("utf-8")
# FIXME: inherited from France Connect mocked RS, bad practice, a bearer token should respect format specified in the RFC
authorization_data = decoded_bearer.split(":")
if len(authorization_data) != 2:
msg = "Token should contain a token and a received_secret."
raise AuthenticationFailed(msg)
token, received_secret = authorization_data
return (token, received_secret)
@staticmethod
def authenticate_service_provider(received_secret):
"""Authenticate the Service Provider (SP) with a shared secret.
This method is temporary, and inspired by Agent Connect mocked resource servers.
"""
if received_secret != get_settings("OIDC_RS_AUTH_SECRET"):
raise AuthenticationFailed("Invalid authentication secret")
@staticmethod
def _get_user(introspection_response):
"""Retrieve the user associated with the given token introspection response."""
sub = introspection_response.get("sub", None)
if sub is None:
raise AuthenticationFailed(
"Introspection response lacks a subject identifier (sub)."
)
user = User.objects.filter(identities__sub=sub).distinct().first()
if user is None:
raise AuthenticationFailed(
"No user found for the given subject identifier (sub)."
)
return sub
def authenticate(self, request):
"""Authenticate the request using a token issued by the OIDC Provider (OP)"""
access_token, received_secret = self._decode_authorization_header(request)
self.authenticate_service_provider(received_secret)
authorization_server = AuthorizationServerClient(
endpoint_introspection=get_settings("OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT"),
endpoint_jwks=get_settings("OIDC_OP_TOKEN_JWKS_ENDPOINT"),
url=get_settings("OIDC_OP_URL"),
verify_ssl=get_settings("OIDC_VERIFY_SSL", True),
timeout=get_settings("OIDC_TIMEOUT", False),
proxy=get_settings("OIDC_PROXY", True),
)
resource_server = ResourceServerClient(
client_id=get_settings("OIDC_RS_CLIENT_ID"),
client_secret=get_settings("OIDC_RS_CLIENT_SECRET"),
encryption_encoding=get_settings("OIDC_RS_ENCRYPTION_ENCODING"),
encryption_algorithm=get_settings("OIDC_RS_ENCRYPTION_ALGO"),
signing_algorithm=get_settings("OIDC_RS_SIGNING_ALGO"),
authorization_server=authorization_server,
)
try:
introspection_response = resource_server.verify(access_token)
except HTTPError as err:
raise AuthenticationFailed(
"Could not fetch introspection response"
) from err
except ValueError as err:
raise AuthenticationFailed("introspection response is not active") from err
user = self._get_user(introspection_response)
return (user, introspection_response)
-219
View File
@@ -1,219 +0,0 @@
"""Resource Server client classes"""
from django.core.exceptions import ImproperlyConfigured, SuspiciousOperation
import requests
from joserfc import jwe as jose_jwe
from joserfc import jwt as jose_jwt
from joserfc.errors import InvalidClaimError
from joserfc.jwk import KeySet
from requests.exceptions import HTTPError
from rest_framework.exceptions import AuthenticationFailed
from .utils import import_private_key_from_settings
class AuthorizationServerClient:
"""Client for interacting with an OAuth 2.0 authorization server.
An authorization server issues access tokens to client applications after authenticating
and obtaining authorization from the resource owner. It also provides endpoints for token
introspection and JSON Web Key Sets (JWKS) to validate and decode tokens.
This client facilitates communication with the authorization server, including:
- Fetching token introspection responses.
- Retrieving JSON Web Key Sets (JWKS) for token validation.
- Setting appropriate headers for secure communication as recommended by RFC drafts.
"""
def __init__(
self, endpoint_introspection, endpoint_jwks, url, verify_ssl, timeout, proxy
):
self.url = url
self._endpoint_introspection = endpoint_introspection
self._endpoint_jwks = endpoint_jwks
self._verify_ssl = verify_ssl
self._timeout = timeout
self._proxy = proxy
@property
def _introspection_headers(self):
"""Get HTTP header for the introspection request.
Notify the authorization server that we expect a signed and encrypted response
by setting the appropriate 'Accept' header. This follows the recommendation from
the draft RFC (https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-introspection-response-12).
"""
return {
"Content-Type": "application/x-www-form-urlencoded",
"Accept": "application/token-introspection+jwt",
}
def fetch_introspection(self, client_id, client_secret, token):
"""Retrieve introspection response about a token."""
response = requests.post(
self._endpoint_introspection,
data={
"client_id": client_id,
"client_secret": client_secret,
"token": token,
},
headers=self._introspection_headers,
verify=self._verify_ssl,
timeout=self._timeout,
proxies=self._proxy,
)
response.raise_for_status()
return response.text
def fetch_jwks(self):
"""Fetch the Json Web Key Set from the jwks endpoint."""
response = requests.get(
self._endpoint_jwks,
verify=self._verify_ssl,
timeout=self._timeout,
proxies=self._proxy,
)
response.raise_for_status()
return response.json()
def retrieve_public_keys(self):
"""Retrieve the public keys from the jwks endpoint."""
try:
jwks = self.fetch_jwks()
except HTTPError as err:
msg = "Failed to retrieve JWKs from the Authorization Server."
raise AuthenticationFailed(msg) from err
try:
public_keys = KeySet.import_key_set(jwks)
except ValueError as err:
msg = (
"Failed to create a public key set from the Authorization Server jwks."
)
raise AuthenticationFailed(msg) from err
return public_keys
class ResourceServerClient:
"""Client for interacting with an OAuth 2.0 resource server.
In the context of OAuth 2.0, a resource server is a server that hosts protected resources and
is capable of accepting and responding to protected resource requests using access tokens.
The resource server verifies the validity of the access tokens issued by the authorization
server to ensure secure access to the resources.
"""
def __init__(
self,
client_id,
client_secret,
encryption_encoding,
encryption_algorithm,
signing_algorithm,
authorization_server,
):
self._client_id = client_id
self._client_secret = client_secret
self._encryption_encoding = encryption_encoding
self._encryption_algorithm = encryption_algorithm
self._signing_algorithm = signing_algorithm
self._authorization_server = authorization_server
def verify(self, token):
"""Verify the token to determine if it's still valid and active.
This method follows the specifications outlined in RFC 7662
(https://www.rfc-editor.org/info/rfc7662) and the draft RFC
(https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-introspection-response-12).
In our eGovernment applications, the standard RFC 7662 does not provide sufficient security.
Its introspection response is a plain JSON object. Therefore, we use the draft RFC that extends
RFC 7662 by returning a signed and encrypted JWT for stronger assurance that the authorization
server issued the token introspection response.
"""
jwe = self._authorization_server.introspect_token(
self._client_id,
self._client_secret,
token,
)
private_key = import_private_key_from_settings()
jws = self._decrypt(jwe, private_key=private_key)
jwt = self._decode(
jws,
public_key_set=self._authorization_server.retrieve_public_keys(),
)
self._validate_claims(jwt)
introspection_response = jwt.claims.get("token_introspection")
active = introspection_response.get("active", None)
if not active:
raise ValueError("Instrospection response is not active.")
return introspection_response
def _decrypt(self, encrypted_token, private_key):
"""Decrypt the token encrypted by the Authorization Server (AS).
Resource Server (RS)'s public key is used for encryption, and its private
key is used for decryption. The RS's public key is exposed to the AS via a JWK endpoint.
Encryption Algorithm and Encoding should be configured to match between the AS
and the RS.
"""
try:
decrypted_token = jose_jwe.decrypt_compact(
encrypted_token,
private_key,
algorithms=[self._encryption_encoding, self._encryption_algorithm],
)
except ValueError as err:
msg = "Token decryption failed."
raise SuspiciousOperation(msg) from err
return decrypted_token
def _decode(self, encoded_token, public_key_set):
"""Decode the token signed by the Authorization Server (AS).
AS's private key is used for signing, and its public key is used for decoding.
The AS public key is exposed via a JWK endpoint.
Signing Algorithm should be configured to match between the AS and the RS.
"""
try:
token = jose_jwt.decode(
encoded_token.plaintext,
public_key_set,
algorithms=[self._signing_algorithm],
)
except ValueError as err:
msg = "Token decoding failed."
raise SuspiciousOperation(msg) from err
return token
def _validate_claims(self, token):
"""Validate the claims of the token to ensure authentication security.
By validating these claims, we ensure that the token was issued by a
trusted authorization server and is intended for this specific
resource server. This prevents various types of attacks, such as
token substitution or misuse of tokens issued for different clients.
"""
claims_requests = jose_jwt.JWTClaimsRegistry(
iss={"essential": True, "value": self._authorization_server.url},
aud={"essential": True, "value": self._client_id},
token_introspection={"essential": True},
)
try:
claims_requests.validate(token.claims)
except InvalidClaimError as err:
msg = "Failed to validate token's claims."
raise SuspiciousOperation(msg) from err
-11
View File
@@ -1,11 +0,0 @@
"""Resource Server URL Configuration"""
from django.urls import path
from .views import DataView, JWKSView
urlpatterns = [
path("jwks", JWKSView.as_view()),
# FIXME: temporary route
path("data", DataView.as_view()),
]
-61
View File
@@ -1,61 +0,0 @@
"""Resource Server utils functions"""
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from joserfc.jwk import JWKRegistry
def import_private_key_from_settings():
"""Import the private key used by the resource server when interacting with the OIDC provider.
This private key is crucial; its public components are exposed in the JWK endpoints,
while its private component is used for decrypting the introspection token retrieved
from the OIDC provider.
By default, we recommend using RSAKey for asymmetric encryption,
known for its strong security features.
Note:
- The function requires the 'OIDC_RS_PRIVATE_KEY_STR' setting to be configured.
- The 'OIDC_RS_ENCRYPTION_KEY_TYPE' and 'OIDC_RS_ENCRYPTION_ALGO' settings can be customized
based on the chosen key type.
Raises:
ImproperlyConfigured: If the private key setting is missing, empty, or incorrect.
Returns:
joserfc.jwk.JWK: The imported private key as a JWK object.
"""
private_key_str = settings.OIDC_RS_PRIVATE_KEY_STR
if not private_key_str:
raise ImproperlyConfigured(
"OIDC_RS_PRIVATE_KEY_STR setting is missing or empty."
)
private_key_pem = private_key_str.encode()
try:
private_key = JWKRegistry.import_key(
private_key_pem,
key_type=settings.OIDC_RS_ENCRYPTION_KEY_TYPE,
parameters={"alg": settings.OIDC_RS_ENCRYPTION_ALGO, "use": "enc"},
)
except ValueError as err:
raise ImproperlyConfigured("OIDC_RS_PRIVATE_KEY_STR setting is wrong.") from err
return private_key
def get_settings(attr, *args):
"""Get the value of a setting from Django settings."""
try:
if args:
return getattr(settings, attr, args[0])
return getattr(settings, attr)
except AttributeError as err:
# FIXME: lint error C0209
msg = "Setting {0} not found".format(attr)
raise ImproperlyConfigured(msg) from err
-54
View File
@@ -1,54 +0,0 @@
"""Resource Server views"""
from joserfc.jwk import KeySet
from rest_framework.response import Response
from rest_framework.views import APIView
from .auth import ResourceServerAuthentication
from .utils import import_private_key_from_settings
class JWKSView(APIView):
"""
API endpoint for retrieving a JSON Web Key (JWK).
Returns:
Response: JSON response containing the JWK data.
"""
authentication_classes = [] # disable authentication
permission_classes = [] # disable permission
def get(self, request):
"""Handle GET requests to retrieve the JSON Web Key (JWK).
Returns:
Response: JSON response containing the JWK data.
"""
private_key = import_private_key_from_settings()
jwk = KeySet([private_key]).as_dict()
return Response(jwk)
# FIXME: temporary view
class DataView(APIView):
"""Temporary view to test resource server authentication."""
authentication_classes = [ResourceServerAuthentication]
permission_classes = [] # disable permission
def get(self, request):
"""Temporary route to test resource server authentication."""
token = request.auth
if 'groups' in token.get('scope'):
# TODO - return a proper error
Response({'error scope "groups" not requested to the authorization server'})
slugs = [team.slug for team in request.user.teams.all()]
# TODO - use SCIM specification to exchange data
return Response({
"groups": slugs
})
@@ -2,12 +2,9 @@
Test for team accesses API endpoints in People's core app : create
"""
import json
import random
import re
import pytest
import responses
from rest_framework.test import APIClient
from core import factories, models
@@ -177,60 +174,3 @@ def test_api_team_accesses_create_authenticated_owner():
"role": role,
"user": str(other_user.id),
}
def test_api_team_accesses_create_webhook():
"""
When the team has a webhook, creating a team access should fire a call.
"""
user, other_user = factories.UserFactory.create_batch(2)
team = factories.TeamFactory(users=[(user, "owner")])
webhook = factories.TeamWebhookFactory(team=team)
role = random.choice([role[0] for role in models.RoleChoices.choices])
client = APIClient()
client.force_login(user)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsp = rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
response = client.post(
f"/api/v1.0/teams/{team.id!s}/accesses/",
{
"user": str(other_user.id),
"role": role,
},
format="json",
)
assert response.status_code == 201
assert rsp.call_count == 1
assert rsps.calls[0].request.url == webhook.url
# Payload sent to scim provider
payload = json.loads(rsps.calls[0].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(other_user.id),
"email": None,
"type": "User",
}
],
}
],
}
@@ -2,12 +2,9 @@
Test for team accesses API endpoints in People's core app : delete
"""
import json
import random
import re
import pytest
import responses
from rest_framework.test import APIClient
from core import factories, models
@@ -151,77 +148,18 @@ def test_api_team_accesses_delete_owners_last_owner():
"""
It should not be possible to delete the last owner access from a team
"""
user = factories.UserFactory()
identity = factories.IdentityFactory()
user = identity.user
team = factories.TeamFactory()
access = factories.TeamAccessFactory(team=team, user=user, role="owner")
assert models.TeamAccess.objects.count() == 1
assert models.TeamAccess.objects.count() == 1
client = APIClient()
client.force_login(user)
response = client.delete(
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 403
assert models.TeamAccess.objects.count() == 1
def test_api_team_accesses_delete_webhook():
"""
When the team has a webhook, deleting a team access should fire a call.
"""
user = factories.UserFactory()
team = factories.TeamFactory(users=[(user, "administrator")])
webhook = factories.TeamWebhookFactory(team=team)
access = factories.TeamAccessFactory(
team=team, role=random.choice(["member", "administrator"])
)
assert models.TeamAccess.objects.count() == 2
assert models.TeamAccess.objects.filter(user=access.user).exists()
client = APIClient()
client.force_login(user)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsp = rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
response = client.delete(
f"/api/v1.0/teams/{team.id!s}/accesses/{access.id!s}/",
)
assert response.status_code == 204
assert rsp.call_count == 1
assert rsps.calls[0].request.url == webhook.url
# Payload sent to scim provider
payload = json.loads(rsps.calls[0].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": None,
"type": "User",
}
],
}
],
}
assert models.TeamAccess.objects.count() == 1
assert models.TeamAccess.objects.filter(user=access.user).exists() is False
@@ -266,15 +266,11 @@ def test_api_team_accesses_list_authenticated_ordering_user(ordering_fields):
assert response.status_code == HTTP_200_OK
assert response.json()["count"] == 21
def normalize(x):
"""Mimic Django order_by, which is case-insensitive and space-insensitive"""
return x.casefold().replace(" ", "")
results = [
team_access["user"][ordering_fields]
for team_access in response.json()["results"]
]
assert sorted(results, key=normalize) == results
assert sorted(results) == results
response = client.get(
f"/api/v1.0/teams/{team.id!s}/accesses/?ordering=-{ordering_fields}",
@@ -286,4 +282,4 @@ def test_api_team_accesses_list_authenticated_ordering_user(ordering_fields):
team_access["user"][ordering_fields]
for team_access in response.json()["results"]
]
assert sorted(results, reverse=True, key=normalize) == results
assert sorted(results, reverse=True) == results
+19 -19
View File
@@ -53,8 +53,8 @@ def test_api_users_authenticated_list_by_email():
Authenticated users should be able to search users with a case-insensitive and
partial query on the email.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="john doe")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -125,8 +125,8 @@ def test_api_users_authenticated_list_by_name():
Authenticated users should be able to search users with a case-insensitive and
partial query on the name.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="john doe")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -192,8 +192,8 @@ def test_api_users_authenticated_list_by_name_and_email():
partial query on the name and email.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="john doe")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -225,7 +225,7 @@ def test_api_users_authenticated_list_exclude_users_already_in_team(
Authenticated users should be able to search users
but the result should exclude all users already in the given team.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -280,8 +280,8 @@ def test_api_users_authenticated_list_multiple_identities_single_user():
"""
User with multiple identities should appear only once in results.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="eva karl")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
client = APIClient()
client.force_login(user)
@@ -308,8 +308,8 @@ def test_api_users_authenticated_list_multiple_identities_multiple_users():
User with multiple identities should be ranked
on their best matching identity.
"""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="john doe")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -368,8 +368,8 @@ def test_api_users_authenticated_list_multiple_identities_multiple_users():
def test_api_users_authenticated_list_uppercase_content():
"""Upper case content should be found by lower case query."""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="eva karl")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
client = APIClient()
client.force_login(user)
@@ -399,8 +399,8 @@ def test_api_users_authenticated_list_uppercase_content():
def test_api_users_list_authenticated_capital_query():
"""Upper case query should find lower case content."""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="eva karl")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
client = APIClient()
client.force_login(user)
@@ -428,8 +428,8 @@ def test_api_users_list_authenticated_capital_query():
def test_api_contacts_list_authenticated_accented_query():
"""Accented content should be found by unaccented query."""
user = factories.UserFactory(admin_email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.admin_email, name="john doe")
user = factories.UserFactory(email="tester@ministry.fr")
factories.IdentityFactory(user=user, email=user.email, name="john doe")
client = APIClient()
client.force_login(user)
@@ -510,7 +510,7 @@ def test_api_users_list_pagination_page_size(
client.force_login(user)
for i in range(page_size):
factories.UserFactory.create(admin_email=f"user-{i}@people.com")
factories.UserFactory.create(email=f"user-{i}@people.com")
response = client.get(
f"/api/v1.0/users/?page_size={page_size}",
@@ -535,7 +535,7 @@ def test_api_users_list_pagination_wrong_page_size(
client.force_login(user)
for i in range(page_size):
factories.UserFactory.create(admin_email=f"user-{i}@people.com")
factories.UserFactory.create(email=f"user-{i}@people.com")
response = client.get(
f"/api/v1.0/users/?page_size={page_size}",
@@ -95,7 +95,7 @@ def test_authentication_getter_existing_user_change_fields(
klass = OIDCAuthenticationBackend()
identity = IdentityFactory(name="John Doe", email="john.doe@example.com")
user_email = identity.user.admin_email
user_email = identity.user.email
# Create multiple identities for a user
for _ in range(5):
@@ -125,7 +125,7 @@ def test_authentication_getter_existing_user_change_fields(
assert models.User.objects.count() == 1
assert user == identity.user
assert user.admin_email == user_email
assert user.email == user_email
def test_authentication_getter_new_user_no_email(monkeypatch):
@@ -148,7 +148,7 @@ def test_authentication_getter_new_user_no_email(monkeypatch):
assert identity.sub == "123"
assert identity.email is None
assert user.admin_email is None
assert user.email is None
assert user.password == "!"
assert models.User.objects.count() == 1
@@ -177,7 +177,7 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
assert identity.email == email
assert identity.name == "John Doe"
assert user.admin_email is None
assert user.email is None
assert models.User.objects.count() == 1
@@ -257,7 +257,7 @@ def test_models_team_invitations_email():
assert len(mail.outbox) == 1
# pylint: disable-next=no-member
email = mail.outbox[0]
(email,) = mail.outbox
assert email.to == [invitation.email]
assert email.subject == "Invitation to join Desk!"
@@ -2,16 +2,12 @@
Unit tests for the TeamAccess model
"""
import json
import re
from django.contrib.auth.models import AnonymousUser
from django.core.exceptions import ValidationError
import pytest
import responses
from core import factories, models
from core import factories
pytestmark = pytest.mark.django_db
@@ -43,94 +39,6 @@ def test_models_team_accesses_unique():
factories.TeamAccessFactory(user=access.user, team=access.team)
def test_models_team_accesses_create_webhook():
"""
When the team has a webhook, creating a team access should fire a call.
"""
user = factories.UserFactory()
team = factories.TeamFactory()
webhook = factories.TeamWebhookFactory(team=team)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsp = rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
models.TeamAccess.objects.create(user=user, team=team)
assert rsp.call_count == 1
assert rsps.calls[0].request.url == webhook.url
# Payload sent to scim provider
payload = json.loads(rsps.calls[0].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(user.id),
"email": None,
"type": "User",
}
],
}
],
}
def test_models_team_accesses_delete_webhook():
"""
When the team has a webhook, deleting a team access should fire a call.
"""
team = factories.TeamFactory()
webhook = factories.TeamWebhookFactory(team=team)
access = factories.TeamAccessFactory(team=team)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsp = rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
access.delete()
assert rsp.call_count == 1
assert rsps.calls[0].request.url == webhook.url
# Payload sent to scim provider
payload = json.loads(rsps.calls[0].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": None,
"type": "User",
}
],
}
],
}
assert models.TeamAccess.objects.exists() is False
# get_abilities
+9 -24
View File
@@ -31,20 +31,20 @@ def test_models_users_id_unique():
def test_models_users_email_unique():
"""The "admin_email" field should be unique except for the null value."""
"""The "email" field should be unique except for the null value."""
user = factories.UserFactory()
with pytest.raises(
ValidationError, match="User with this Admin email address already exists."
ValidationError, match="User with this Email address already exists."
):
models.User.objects.create(admin_email=user.admin_email, password="password")
models.User.objects.create(email=user.email)
def test_models_users_email_several_null():
"""Several users with a null value for the "email" field can co-exist."""
factories.UserFactory(admin_email=None)
models.User.objects.create(admin_email=None, password="foo.")
factories.UserFactory(email=None)
models.User.objects.create(email=None, password="foo.")
assert models.User.objects.filter(admin_email__isnull=True).count() == 2
assert models.User.objects.filter(email__isnull=True).count() == 2
def test_models_users_profile_not_owned():
@@ -91,26 +91,11 @@ def test_models_users_send_mail_main_existing():
)
def test_models_users_send_mail_main_admin():
"""
The 'email_user' method should send mail to the user's admin email address if the
user has no related identities.
"""
user = factories.UserFactory()
with mock.patch("django.core.mail.send_mail") as mock_send:
user.email_user("my subject", "my message")
mock_send.assert_called_once_with(
"my subject", "my message", None, [user.admin_email]
)
def test_models_users_send_mail_main_missing():
"""The 'email_user' method should fail if the user has no email address."""
user = factories.UserFactory(admin_email=None)
user = factories.UserFactory()
with pytest.raises(ValueError) as excinfo:
with pytest.raises(models.Identity.DoesNotExist) as excinfo:
user.email_user("my subject", "my message")
assert str(excinfo.value) == "You must first set an email for the user."
assert str(excinfo.value) == "Identity matching query does not exist."
@@ -1,341 +0,0 @@
"""Test Team synchronization webhooks."""
import json
import random
import re
from logging import Logger
from unittest import mock
import pytest
import responses
from core import factories
from core.utils.webhooks import scim_synchronizer
pytestmark = pytest.mark.django_db
def test_utils_webhooks_add_user_to_group_no_webhooks():
"""If no webhook is declared on the team, the function should not make any request."""
access = factories.TeamAccessFactory()
with responses.RequestsMock():
scim_synchronizer.add_user_to_group(access.team, access.user)
assert len(responses.calls) == 0
@mock.patch.object(Logger, "info")
def test_utils_webhooks_add_user_to_group_success(mock_info):
"""The user passed to the function should get added."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhooks = factories.TeamWebhookFactory.create_batch(2, team=access.team)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
scim_synchronizer.add_user_to_group(access.team, access.user)
for i, webhook in enumerate(webhooks):
assert rsps.calls[i].request.url == webhook.url
# Check headers
headers = rsps.calls[i].request.headers
assert "Authorization" not in headers
assert headers["Content-Type"] == "application/json"
# Payload sent to scim provider
for call in rsps.calls:
payload = json.loads(call.request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": identity.email,
"type": "User",
}
],
}
],
}
# Logger
assert mock_info.call_count == 2
for i, webhook in enumerate(webhooks):
assert mock_info.call_args_list[i][0] == (
"%s synchronization succeeded with %s",
"add_user_to_group",
webhook.url,
)
# Status
for webhook in webhooks:
webhook.refresh_from_db()
assert webhook.status == "success"
@mock.patch.object(Logger, "info")
def test_utils_webhooks_remove_user_from_group_success(mock_info):
"""The user passed to the function should get removed."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhooks = factories.TeamWebhookFactory.create_batch(2, team=access.team)
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
scim_synchronizer.remove_user_from_group(access.team, access.user)
for i, webhook in enumerate(webhooks):
assert rsps.calls[i].request.url == webhook.url
# Payload sent to scim provider
for call in rsps.calls:
payload = json.loads(call.request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": identity.email,
"type": "User",
}
],
}
],
}
# Logger
assert mock_info.call_count == 2
for i, webhook in enumerate(webhooks):
assert mock_info.call_args_list[i][0] == (
"%s synchronization succeeded with %s",
"remove_user_from_group",
webhook.url,
)
# Status
for webhook in webhooks:
webhook.refresh_from_db()
assert webhook.status == "success"
@mock.patch.object(Logger, "error")
@mock.patch.object(Logger, "info")
def test_utils_webhooks_add_user_to_group_failure(mock_info, mock_error):
"""The logger should be called on webhook call failure."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhooks = factories.TeamWebhookFactory.create_batch(2, team=access.team)
with responses.RequestsMock() as rsps:
# Simulate webhook failure using "responses":
rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=random.choice([404, 301, 302]),
content_type="application/json",
)
scim_synchronizer.add_user_to_group(access.team, access.user)
for i, webhook in enumerate(webhooks):
assert rsps.calls[i].request.url == webhook.url
# Payload sent to scim provider
for call in rsps.calls:
payload = json.loads(call.request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": identity.email,
"type": "User",
}
],
}
],
}
# Logger
assert not mock_info.called
assert mock_error.call_count == 2
for i, webhook in enumerate(webhooks):
assert mock_error.call_args_list[i][0] == (
"%s synchronization failed with %s",
"add_user_to_group",
webhook.url,
)
# Status
for webhook in webhooks:
webhook.refresh_from_db()
assert webhook.status == "failure"
@mock.patch.object(Logger, "error")
@mock.patch.object(Logger, "info")
def test_utils_webhooks_add_user_to_group_retries(mock_info, mock_error):
"""webhooks synchronization supports retries."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhook = factories.TeamWebhookFactory(team=access.team)
url = re.compile(r".*/Groups/.*")
with responses.RequestsMock() as rsps:
# Make webhook fail 3 times before succeeding using "responses"
all_rsps = [
rsps.add(rsps.PATCH, url, status=500, content_type="application/json"),
rsps.add(rsps.PATCH, url, status=500, content_type="application/json"),
rsps.add(rsps.PATCH, url, status=500, content_type="application/json"),
rsps.add(rsps.PATCH, url, status=200, content_type="application/json"),
]
scim_synchronizer.add_user_to_group(access.team, access.user)
for i in range(4):
assert all_rsps[i].call_count == 1
assert rsps.calls[i].request.url == webhook.url
payload = json.loads(rsps.calls[i].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": identity.email,
"type": "User",
}
],
}
],
}
# Logger
assert not mock_error.called
assert mock_info.call_count == 1
assert mock_info.call_args_list[0][0] == (
"%s synchronization succeeded with %s",
"add_user_to_group",
webhook.url,
)
# Status
webhook.refresh_from_db()
assert webhook.status == "success"
@mock.patch.object(Logger, "error")
@mock.patch.object(Logger, "info")
def test_utils_synchronize_course_runs_max_retries_exceeded(mock_info, mock_error):
"""Webhooks synchronization has exceeded max retries and should get logged."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhook = factories.TeamWebhookFactory(team=access.team)
with responses.RequestsMock() as rsps:
# Simulate webhook temporary failure using "responses":
rsp = rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=random.choice([500, 502]),
content_type="application/json",
)
scim_synchronizer.add_user_to_group(access.team, access.user)
assert rsp.call_count == 5
assert rsps.calls[0].request.url == webhook.url
payload = json.loads(rsps.calls[0].request.body)
assert payload == {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{
"value": str(access.user.id),
"email": identity.email,
"type": "User",
}
],
}
],
}
# Logger
assert not mock_info.called
assert mock_error.call_count == 1
assert mock_error.call_args_list[0][0] == (
"%s synchronization failed due to max retries exceeded with url %s",
"add_user_to_group",
webhook.url,
)
# Status
webhook.refresh_from_db()
assert webhook.status == "failure"
def test_utils_webhooks_add_user_to_group_authorization():
"""Secret token should be passed in authorization header when set."""
identity = factories.IdentityFactory()
access = factories.TeamAccessFactory(user=identity.user)
webhook = factories.TeamWebhookFactory(team=access.team, secret="123")
with responses.RequestsMock() as rsps:
# Ensure successful response by scim provider using "responses":
rsps.add(
rsps.PATCH,
re.compile(r".*/Groups/.*"),
body="{}",
status=200,
content_type="application/json",
)
scim_synchronizer.add_user_to_group(access.team, access.user)
assert rsps.calls[0].request.url == webhook.url
# Check headers
headers = rsps.calls[0].request.headers
assert headers["Authorization"] == "Bearer 123"
assert headers["Content-Type"] == "application/json"
# Status
webhook.refresh_from_db()
assert webhook.status == "success"
View File
-70
View File
@@ -1,70 +0,0 @@
"""A minimalist SCIM client to synchronize with remote service providers."""
import logging
import requests
from urllib3.util import Retry
logger = logging.getLogger(__name__)
adapter = requests.adapters.HTTPAdapter(
max_retries=Retry(
total=4,
backoff_factor=0.1,
status_forcelist=[500, 502],
allowed_methods=["PATCH"],
)
)
session = requests.Session()
session.mount("http://", adapter)
session.mount("https://", adapter)
class SCIMClient:
"""A minimalist SCIM client for our needs."""
def add_user_to_group(self, webhook, user):
"""Add a user to a group from its ID or email."""
payload = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "add",
"path": "members",
"value": [
{"value": str(user.id), "email": user.email, "type": "User"}
],
}
],
}
return session.patch(
webhook.url,
json=payload,
headers=webhook.get_headers(),
verify=False,
timeout=3,
)
def remove_user_from_group(self, webhook, user):
"""Remove a user from a group by its ID or email."""
payload = {
"schemas": ["urn:ietf:params:scim:api:messages:2.0:PatchOp"],
"Operations": [
{
"op": "remove",
"path": "members",
"value": [
{"value": str(user.id), "email": user.email, "type": "User"}
],
}
],
}
return session.patch(
webhook.url,
json=payload,
headers=webhook.get_headers(),
verify=False,
timeout=3,
)
-75
View File
@@ -1,75 +0,0 @@
"""Fire webhooks with synchronous retries"""
import logging
import requests
from core.enums import WebhookStatusChoices
from .scim import SCIMClient
logger = logging.getLogger(__name__)
class WebhookSCIMClient:
"""Wraps the SCIM client to record call results on webhooks."""
def __getattr__(self, name):
"""Handle calls from webhooks to synchronize a team access with a distant application."""
def wrapper(team, user):
"""
Wrap SCIMClient calls to handle retries, error handling and storing result in the
calling Webhook instance.
"""
for webhook in team.webhooks.all():
if not webhook.url:
continue
client = SCIMClient()
status = WebhookStatusChoices.FAILURE
try:
response = getattr(client, name)(webhook, user)
except requests.exceptions.RetryError as exc:
logger.error(
"%s synchronization failed due to max retries exceeded with url %s",
name,
webhook.url,
exc_info=exc,
)
except requests.exceptions.RequestException as exc:
logger.error(
"%s synchronization failed with %s.",
name,
webhook.url,
exc_info=exc,
)
else:
extra = {
"response": response.content,
}
# pylint: disable=no-member
if response.status_code == requests.codes.ok:
logger.info(
"%s synchronization succeeded with %s",
name,
webhook.url,
extra=extra,
)
status = WebhookStatusChoices.SUCCESS
else:
logger.error(
"%s synchronization failed with %s",
name,
webhook.url,
extra=extra,
)
webhook._meta.model.objects.filter(id=webhook.id).update(status=status) # noqa
return wrapper
scim_synchronizer = WebhookSCIMClient()
@@ -115,7 +115,7 @@ def create_demo(stdout):
for i in range(defaults.NB_OBJECTS["users"]):
queue.push(
models.User(
admin_email=f"user{i:d}@example.com",
email=f"user{i:d}@example.com",
password="!",
is_superuser=False,
is_active=True,
@@ -126,12 +126,12 @@ def create_demo(stdout):
queue.flush()
with Timeit(stdout, "Creating identities"):
users_values = list(models.User.objects.values("id", "admin_email"))
users_values = list(models.User.objects.values("id", "email"))
for user_dict in users_values:
for i in range(
random.choices(range(5), weights=[5, 50, 30, 10, 5], k=1)[0]
):
user_email = user_dict["admin_email"]
user_email = user_dict["email"]
queue.push(
models.Identity(
user_id=user_dict["id"],
@@ -17,7 +17,7 @@ class Command(BaseCommand):
def add_arguments(self, parser):
"""Define required arguments "email" and "password"."""
parser.add_argument(
"--admin_email",
"--email",
help=("Email for the user."),
)
parser.add_argument(
@@ -30,11 +30,11 @@ class Command(BaseCommand):
Given an email and a password, create a superuser or upgrade the existing
user to superuser status.
"""
email = options.get("admin_email")
email = options.get("email")
try:
user = User.objects.get(admin_email=email)
user = User.objects.get(email=email)
except User.DoesNotExist:
user = User(admin_email=email)
user = User(email=email)
message = "Superuser created successfully."
else:
if user.is_superuser and user.is_staff:
@@ -31,14 +31,3 @@ def test_commands_create_demo():
assert models.Identity.objects.exists()
assert models.Team.objects.count() == 3
assert models.TeamAccess.objects.count() >= 3
def test_commands_createsuperuser():
"""
The createsuperuser management command should create a user
with superuser permissions.
"""
call_command("createsuperuser")
assert models.User.objects.count() == 1
-2
View File
@@ -7,7 +7,6 @@ from mozilla_django_oidc.urls import urlpatterns as oidc_urls
from rest_framework.routers import DefaultRouter
from core.api import viewsets
from core.resource_server.urls import urlpatterns as resource_server_urls
# - Main endpoints
router = DefaultRouter()
@@ -36,7 +35,6 @@ urlpatterns = [
[
*router.urls,
*oidc_urls,
*resource_server_urls,
re_path(
r"^teams/(?P<team_id>[0-9a-z-]*)/",
include(team_related_router.urls),
+27 -70
View File
@@ -268,7 +268,7 @@ class Base(Configuration):
EMAIL_PORT = values.PositiveIntegerValue(None)
EMAIL_USE_TLS = values.BooleanValue(False)
EMAIL_USE_SSL = values.BooleanValue(False)
EMAIL_FROM = values.Value("from@example.com")
EMAIL_FROM = values.Value("do-not-reply@desk.beta.numerique.gouv.fr")
AUTH_USER_MODEL = "core.User"
INVITATION_VALIDITY_DURATION = 604800 # 7 days, in seconds
@@ -301,9 +301,34 @@ class Base(Configuration):
default=True,
environ_name="OIDC_CREATE_USER",
)
OIDC_RP_SIGN_ALGO = values.Value(
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
)
OIDC_RP_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = values.Value(
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_OP_JWKS_ENDPOINT = values.Value(
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
)
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
)
OIDC_OP_TOKEN_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
)
OIDC_AUTH_REQUEST_EXTRA_PARAMS = values.DictValue(
{}, environ_name="OIDC_AUTH_REQUEST_EXTRA_PARAMS", environ_prefix=None
)
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
LOGIN_REDIRECT_URL = values.Value(
None, environ_name="LOGIN_REDIRECT_URL", environ_prefix=None
)
@@ -322,81 +347,13 @@ class Base(Configuration):
OIDC_REDIRECT_ALLOWED_HOSTS = values.ListValue(
default=[], environ_name="OIDC_REDIRECT_ALLOWED_HOSTS", environ_prefix=None
)
USER_OIDC_FIELDS_TO_NAME = values.ListValue(
default=["first_name", "last_name"],
environ_name="USER_OIDC_FIELDS_TO_NAME",
environ_prefix=None,
)
# OIDC - Resource Provider (RP) also named Service Provider (SP)
OIDC_RP_SCOPES = values.Value(
"openid email", environ_name="OIDC_RP_SCOPES", environ_prefix=None
)
OIDC_RP_SIGN_ALGO = values.Value(
"RS256", environ_name="OIDC_RP_SIGN_ALGO", environ_prefix=None
)
OIDC_RP_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RP_CLIENT_ID", environ_prefix=None
)
OIDC_RP_CLIENT_SECRET = values.Value(
environ_name="OIDC_RP_CLIENT_SECRET",
environ_prefix=None,
)
# OIDC - OIDC Provider (OP)
OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_INTROSPECTION_ENDPOINT", environ_prefix=None
)
OIDC_OP_JWKS_ENDPOINT = values.Value(
environ_name="OIDC_OP_JWKS_ENDPOINT", environ_prefix=None
)
OIDC_OP_AUTHORIZATION_ENDPOINT = values.Value(
environ_name="OIDC_OP_AUTHORIZATION_ENDPOINT", environ_prefix=None
)
OIDC_OP_TOKEN_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_TOKEN_ENDPOINT", environ_prefix=None
)
OIDC_OP_USER_ENDPOINT = values.Value(
None, environ_name="OIDC_OP_USER_ENDPOINT", environ_prefix=None
)
# TODO: refactor this settings, should be factorized with all endpoints
OIDC_OP_URL = values.Value(None, environ_name="OIDC_OP_URL", environ_prefix=None)
# OIDC - Resource Server (RS)
OIDC_RS_CLIENT_ID = values.Value(
"people", environ_name="OIDC_RS_CLIENT_ID", environ_prefix=None
)
OIDC_RS_CLIENT_SECRET = values.Value(
environ_name="OIDC_RS_CLIENT_SECRET",
environ_prefix=None,
)
OIDC_RS_AUTH_SECRET = values.Value(
None, environ_name="OIDC_RS_AUTH_SECRET", environ_prefix=None
)
OIDC_RS_PRIVATE_KEY_STR = values.Value(
default=None,
environ_name="OIDC_RS_PRIVATE_KEY_STR",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ALGO = values.Value(
default="RSA-OAEP",
environ_name="OIDC_RS_ENCRYPTION_ALGO",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_ENCODING = values.Value(
default="A256GCM",
environ_name="OIDC_RS_ENCRYPTION_ENCODING",
environ_prefix=None,
)
OIDC_RS_ENCRYPTION_KEY_TYPE = values.Value(
default="RSA",
environ_name="OIDC_RS_ENCRYPTION_KEY_TYPE",
environ_prefix=None,
)
OIDC_RS_SIGNING_ALG0 = values.Value(
default="ES256", environ_name="OIDC_RS_SIGNING_ALG0", environ_prefix=None
)
# pylint: disable=invalid-name
@property
def ENVIRONMENT(self):
+7 -8
View File
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"boto3==1.34.84",
"boto3==1.34.75",
"Brotli==1.1.0",
"celery[redis]==5.3.6",
"django-configurations==2.5.1",
@@ -36,10 +36,10 @@ dependencies = [
"django-redis==5.4.0",
"django-storages==1.14.2",
"django-timezone-field>=5.1",
"django==5.0.4",
"django==5.0.3",
"djangorestframework==3.15.1",
"drf_spectacular==0.27.2",
"dockerflow==2024.4.1",
"dockerflow==2024.3.0",
"easy_thumbnails==2.8.5",
"factory_boy==3.3.0",
"gunicorn==21.2.0",
@@ -47,9 +47,8 @@ dependencies = [
"nested-multipart-parser==1.5.0",
"psycopg[binary]==3.1.18",
"PyJWT==2.8.0",
"joserfc==0.9.0",
"requests==2.31.0",
"sentry-sdk==1.45.0",
"sentry-sdk==1.44.0",
"url-normalize==1.4.3",
"whitenoise==6.6.0",
"mozilla-django-oidc==4.0.1",
@@ -67,7 +66,7 @@ dev = [
"drf-spectacular-sidecar==2024.4.1",
"ipdb==0.13.13",
"ipython==8.23.0",
"pyfakefs==5.4.1",
"pyfakefs==5.3.5",
"pylint-django==2.5.5",
"pylint==3.1.0",
"pytest-cov==5.0.0",
@@ -76,8 +75,8 @@ dev = [
"pytest-icdiff==0.9",
"pytest-xdist==3.5.0",
"responses==0.25.0",
"ruff==0.3.7",
"types-requests==2.31.0.20240406",
"ruff==0.3.5",
"types-requests==2.31.0.20240402",
"freezegun==1.4.0",
]
+1
View File
@@ -33,3 +33,4 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
-1
View File
@@ -1 +0,0 @@
next-env.d.ts
-5
View File
@@ -1,5 +0,0 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
+4 -4
View File
@@ -8,7 +8,7 @@
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes",
"start": "npx -y serve@latest out",
"lint": "tsc --noEmit && next lint",
"lint": "next lint",
"prettier": "prettier --write .",
"stylelint": "stylelint \"**/*.css\"",
"test": "jest",
@@ -16,7 +16,7 @@
},
"dependencies": {
"@openfun/cunningham-react": "2.7.0",
"@tanstack/react-query": "5.29.0",
"@tanstack/react-query": "5.28.9",
"i18next": "23.10.1",
"lodash": "4.17.21",
"luxon": "3.4.4",
@@ -31,7 +31,7 @@
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.29.0",
"@tanstack/react-query-devtools": "5.28.10",
"@testing-library/jest-dom": "6.4.2",
"@testing-library/react": "14.2.2",
"@testing-library/user-event": "14.5.2",
@@ -39,7 +39,7 @@
"@types/lodash": "4.17.0",
"@types/luxon": "3.4.2",
"@types/node": "*",
"@types/react": "18.2.74",
"@types/react": "18.2.73",
"@types/react-dom": "*",
"dotenv": "16.4.5",
"eslint-config-people": "*",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

@@ -34,22 +34,25 @@ describe('fetchAPI', () => {
});
it('logout if 401 response', async () => {
useAuthStore.setState({
authenticated: true,
userData: { id: '123', email: 'test@test.com' },
const mockReplace = jest.fn();
Object.defineProperty(window, 'location', {
configurable: true,
enumerable: true,
value: {
replace: mockReplace,
},
});
useAuthStore.setState({ userData: { email: 'test@test.com' } });
fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401);
fetchMock.mock('http://some.api.url/api/v1.0/logout/', 302);
await fetchAPI('some/url');
await Promise.all([fetchMock.flush()]);
expect(useAuthStore.getState().userData).toBeUndefined();
expect(fetchMock.lastUrl()).toEqual('http://some.api.url/api/v1.0/logout/');
const { userData, authenticated } = useAuthStore.getState();
expect(userData).toBeUndefined();
expect(authenticated).toBeFalsy();
expect(mockReplace).toHaveBeenCalledWith(
'http://some.api.url/api/v1.0/authenticate/',
);
});
});
+5 -1
View File
@@ -1,4 +1,4 @@
import { useAuthStore } from '@/core/auth';
import { login, useAuthStore } from '@/core/auth';
/**
* Retrieves the CSRF token from the document's cookies.
@@ -29,8 +29,12 @@ export const fetchAPI = async (input: string, init?: RequestInit) => {
},
});
// todo - handle 401, redirect to login screen
// todo - please have a look to this documentation page https://mozilla-django-oidc.readthedocs.io/en/stable/xhr.html
if (response.status === 401) {
logout();
// Fix - force re-logging the user, will be refactored
login();
}
return response;
+17 -15
View File
@@ -1,28 +1,30 @@
import { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { useCunninghamTheme } from '@/cunningham';
import { Box, BoxType } from '.';
import { BoxType } from '.';
export const Card = ({
children,
$css,
...props
}: PropsWithChildren<BoxType>) => {
const Wrapper = styled.div`
position: relative;
background-color: white;
border-radius: 0.25rem;
box-shadow: 2px 2px 5px var(--shadow-color) 88;
border: 1px solid var(--border-color);
`;
export const Card = ({ children, ...props }: PropsWithChildren<BoxType>) => {
const { colorsTokens } = useCunninghamTheme();
return (
<Box
$background="white"
$radius="4px"
$css={`
box-shadow: 2px 2px 5px ${colorsTokens()['primary-300']}88;
border: 1px solid ${colorsTokens()['card-border']};
${$css}
`}
<Wrapper
style={{
'--border-color': colorsTokens()['card-border'],
'--shadow-color': colorsTokens()['primary-300'],
}}
{...props}
>
{children}
</Box>
</Wrapper>
);
};
@@ -23,10 +23,6 @@ const StyledButton = styled(Button)`
background: none;
outline: none;
transition: all 0.2s ease-in-out;
font-family: Marianne, Arial, serif;
font-weight: 500;
font-size: 0.938rem;
text-wrap: nowrap;
`;
interface DropButtonProps {
+21 -9
View File
@@ -1,19 +1,31 @@
import { PropsWithChildren } from 'react';
import styled from 'styled-components';
import { Box } from '@/components';
import { HEADER_HEIGHT, Header } from '@/features/header';
import { Menu } from '@/features/menu';
import { MENU_WIDTH, Menu } from '@/features/menu';
export function MainLayout({ children }: PropsWithChildren) {
const Container = styled.div`
display: flex;
flex-direction: row;
flex: 1;
`;
const Main = styled.main`
display: flex;
flex-wrap: nowrap;
flex-basis: 100%;
height: calc(100vh - ${HEADER_HEIGHT});
max-width: calc(100% - ${MENU_WIDTH});
`;
export function MainLayout({ children }: { children: React.ReactNode }) {
return (
<Box $height="100vh" $css="overflow:hidden;">
<Box>
<Header />
<Box $css="flex: 1;" $direction="row">
<Container>
<Menu />
<Box as="main" $height={`calc(100vh - ${HEADER_HEIGHT})`} $width="100%">
{children}
</Box>
</Box>
<Main>{children}</Main>
</Container>
</Box>
);
}
@@ -10,7 +10,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
useEffect(() => {
initAuth();
}, [initAuth, authenticated]);
}, [initAuth]);
if (!authenticated) {
return (
@@ -1,3 +1,2 @@
export * from './types';
export * from './getMe';
export * from './logout';
export * from './types';
@@ -1,8 +0,0 @@
import { fetchAPI } from '@/api';
export const logout = async () => {
await fetchAPI(`logout/`, {
method: 'POST',
redirect: 'manual',
});
};
@@ -1,6 +1,6 @@
import { create } from 'zustand';
import { User, getMe, logout } from './api';
import { User, getMe } from './api';
export const login = () => {
window.location.replace(
@@ -30,12 +30,11 @@ export const useAuthStore = create<AuthStore>((set) => ({
set({ authenticated: true, userData: data });
})
.catch(() => {
// todo - implement a proper login screen to prevent automatic navigation.
login();
});
},
logout: () => {
void logout().then(() => {
set(initialState);
});
set(initialState);
},
}));
@@ -110,9 +110,7 @@ export const SearchMembers = ({
onInputChange={onInputChangeHandle}
inputValue={input}
placeholder={t('Search new members (name or email)')}
noOptionsMessage={() =>
t('Invite new members to {{teamName}}', { teamName: team.name })
}
noOptionsMessage={() => t('Invite new members to your team')}
onChange={(value) => {
setInput('');
setUserQuery('');
@@ -1,34 +0,0 @@
import { Button } from '@openfun/cunningham-react';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { Box, DropButton, Text } from '@/components';
import { useAuthStore } from '@/core/auth';
export const AccountDropdown = () => {
const { t } = useTranslation();
const { logout } = useAuthStore();
return (
<DropButton
aria-label={t('My account')}
button={
<Box $flex $direction="row" $align="center">
<Text $theme="primary">{t('My account')}</Text>
<Text className="material-icons" $theme="primary">
arrow_drop_down
</Text>
</Box>
}
>
<Button
onClick={logout}
color="primary-text"
icon={<span className="material-icons">logout</span>}
aria-label={t('Logout')}
>
<Text $weight="normal">{t('Logout')}</Text>
</Button>
</DropButton>
);
};
@@ -4,14 +4,14 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, Text } from '@/components/';
import { AccountDropdown } from '@/features/header/AccountDropdown';
import { ApplicationsMenu } from '@/features/header/ApplicationsMenu';
import { LanguagePicker } from '../language/';
import { default as IconApplication } from './assets/icon-application.svg?url';
import { default as IconDesk } from './assets/icon-desk.svg?url';
import { default as IconGouv } from './assets/icon-gouv.svg?url';
import { default as IconMarianne } from './assets/icon-marianne.svg?url';
import IconMyAccount from './assets/icon-my-account.png';
export const HEADER_HEIGHT = '100px';
@@ -55,17 +55,42 @@ export const Header = () => {
alt={t('Freedom Equality Fraternity Logo')}
/>
<Box $align="center" $gap="1rem" $direction="row">
<Image priority src={IconApplication} alt={t('Equipes Logo')} />
<Image priority src={IconDesk} alt={t('Desk Logo')} />
<Text className="m-0" as="h2" $theme="primary">
{t('Equipes')}
{t('Desk')}
</Text>
</Box>
</Box>
</Box>
<Box $align="center" $gap="1rem" $justify="flex-end" $direction="row">
<AccountDropdown />
<LanguagePicker />
<ApplicationsMenu />
<Box
$align="center"
$css={`
& > button {
padding: 0;
}
`}
$gap="5rem"
$justify="flex-end"
$direction="row"
>
<Box $align="center" $direction="row">
<LanguagePicker />
</Box>
<Box $direction="row" $align="center">
<Box $direction="row" $align="center" $gap="1rem">
<Text $weight="bold" $theme="primary">
John Doe
</Text>
<Image
width={58}
height={58}
priority
src={IconMyAccount}
alt={t(`Profile picture`)}
/>
</Box>
<ApplicationsMenu />
</Box>
</Box>
</Box>
</StyledHeader>
@@ -1,17 +0,0 @@
<svg viewBox="0 0 41 39" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M35.7941 20.6945C33.8535 20.6945 32.052 19.4495 31.4319 17.4987C30.6646 15.0855 31.9962 12.5063 34.406 11.7379C34.868 11.5906 35.3361 11.5205 35.7965 11.5205C37.7371 11.5205 39.5386 12.7656 40.1587 14.7161C40.9261 17.1295 39.5944 19.7087 37.1846 20.4772C36.7227 20.6244 36.2544 20.6945 35.7941 20.6945Z" fill="#000091"/>
<path d="M34.5271 27.8442C35.25 27.4943 35.7975 26.809 35.9377 25.9554C36.1665 24.5616 35.2238 23.2458 33.832 23.0167C33.0544 22.8887 32.2816 22.8267 31.5179 22.8267C30.0882 22.8267 28.6926 23.0442 27.3699 23.452C26.4866 22.2232 25.8084 20.8654 25.3541 19.4365C24.8882 17.9708 24.6574 16.4304 24.6814 14.8774C26.7391 14.2179 28.6763 13.0776 30.315 11.4558C30.8765 10.9003 31.1266 10.1487 31.0647 9.41846C31.1336 9.9223 31.0533 10.4517 30.8025 10.9377C28.4985 15.4006 30.2496 20.9084 34.7064 23.2161C35.9591 23.8649 36.4499 25.4081 35.8021 26.6625C35.5168 27.2153 35.058 27.6199 34.5271 27.8442Z" fill="#000091"/>
<path d="M29.6422 38.9999C28.2165 38.9999 26.8121 38.3354 25.9176 37.0873C24.4433 35.0295 24.9138 32.1644 26.9685 30.6878C27.7768 30.107 28.7102 29.8276 29.6343 29.8276C31.0598 29.8276 32.4644 30.4919 33.3587 31.7402C34.8332 33.798 34.3628 36.6631 32.3081 38.1395C31.4997 38.7205 30.5664 38.9999 29.6422 38.9999Z" fill="#E1000F"/>
<path d="M20.0313 37.7184C19.8934 37.7184 19.7541 37.7071 19.6136 37.6842C19.0005 37.583 18.4746 37.2713 18.0977 36.8349C18.5686 37.3268 19.229 37.6241 19.9395 37.6241C20.0673 37.6241 20.1968 37.6145 20.3271 37.5946C21.7217 37.3818 22.6799 36.0774 22.4675 34.681C22.1266 32.4399 21.2783 30.4005 20.0573 28.6611C20.9513 27.4401 22.0312 26.3748 23.2479 25.5005C24.4958 24.6038 25.8874 23.9079 27.3698 23.4512C28.632 25.2071 30.3135 26.6994 32.3601 27.759C32.7353 27.9534 33.1362 28.0454 33.5312 28.0454C33.8743 28.0454 34.2133 27.9757 34.5269 27.8433C34.1886 28.0071 33.8119 28.0976 33.4204 28.0976C33.2828 28.0976 33.1432 28.0862 33.0029 28.0633C32.5052 27.9811 32.0101 27.9416 31.5212 27.9416C27.1486 27.9416 23.2809 31.1162 22.5482 35.5754C22.3423 36.8286 21.2591 37.7184 20.0313 37.7184Z" fill="#E1000F"/>
<path d="M33.5248 28.0462C33.1297 28.0462 32.7288 27.9543 32.3537 27.7599C30.307 26.7003 28.6255 25.208 27.3633 23.452C28.686 23.0442 30.0815 22.8267 31.5113 22.8267C32.2749 22.8267 33.0477 22.8887 33.8253 23.0167C35.2172 23.2458 36.1599 24.5616 35.9311 25.9554C35.7908 26.809 35.2433 27.4942 34.5205 27.8442C34.2068 27.9765 33.8679 28.0462 33.5248 28.0462Z" fill="#6D2700"/>
<path d="M10.3416 38.7923C9.39936 38.7923 8.44894 38.5023 7.63175 37.9003C5.59448 36.3995 5.15785 33.5291 6.65647 31.489C7.55399 30.2671 8.94222 29.6201 10.3489 29.6201C11.2912 29.6201 12.2416 29.9104 13.0588 30.5124C15.096 32.013 15.5327 34.8836 14.034 36.9237C13.1366 38.1454 11.7483 38.7923 10.3416 38.7923Z" fill="#E1000F"/>
<path d="M18.0934 36.8362C17.7418 36.4693 17.4958 35.9937 17.4135 35.4527C16.7284 30.9489 12.8409 27.7125 8.42383 27.7125C7.96983 27.7125 7.51056 27.7468 7.04827 27.8171C6.91806 27.837 6.78862 27.8466 6.6608 27.8466C5.42004 27.8466 4.33134 26.9397 4.13887 25.6739C4.04527 25.0586 4.17885 24.4614 4.47689 23.9673C4.0973 24.6761 4.05657 25.5525 4.4443 26.3257C4.89261 27.2199 5.79338 27.7363 6.72875 27.7363C7.11393 27.7363 7.5051 27.6487 7.87279 27.4637C9.89584 26.4467 11.5707 25.0083 12.8452 23.3081C14.2811 23.7821 15.6267 24.4815 16.8329 25.3701C18.0701 26.2816 19.1611 27.3918 20.053 28.6624C18.7755 30.4072 17.8777 32.4698 17.5038 34.7465C17.3756 35.5265 17.6144 36.2821 18.0934 36.8362Z" fill="#E1000F"/>
<path d="M19.9345 37.6241C19.224 37.6241 18.5636 37.3268 18.0926 36.8349C17.6137 36.2808 17.3748 35.5252 17.5031 34.7452C17.8769 32.4685 18.7747 30.4059 20.0522 28.6611C21.2733 30.4005 22.1216 32.4399 22.4625 34.681C22.6749 36.0774 21.7167 37.3819 20.3221 37.5946C20.1918 37.6145 20.0623 37.6241 19.9345 37.6241Z" fill="#6D2700"/>
<path d="M4.58127 20.3585C4.1036 20.3585 3.618 20.2832 3.13975 20.1244C0.739113 19.3276 -0.561933 16.7329 0.233815 14.3288C0.871033 12.4037 2.6598 11.1846 4.57985 11.1846C5.05753 11.1846 5.54314 11.26 6.02139 11.4187C8.42203 12.2156 9.72307 14.8105 8.92732 17.2145C8.29011 19.1395 6.50134 20.3585 4.58127 20.3585Z" fill="#000091"/>
<path d="M4.48047 23.9669C4.72047 23.5187 5.09596 23.1376 5.58427 22.8921C10.0678 20.6379 11.8839 15.1512 9.63285 10.6609C9.00015 9.39848 9.50889 7.86127 10.7693 7.22763C11.137 7.04273 11.5283 6.95508 11.9135 6.95508C12.11 6.95508 12.305 6.97788 12.4945 7.02216C12.3443 6.99509 12.192 6.98153 12.0397 6.98153C11.3912 6.98153 10.7423 7.22739 10.2446 7.71995C9.2413 8.71285 9.23173 10.3323 10.2233 11.3369C11.8145 12.9493 13.6979 14.1001 15.7066 14.7884C15.7001 16.3025 15.4516 17.8001 14.9805 19.2236C14.4972 20.6835 13.7798 22.0655 12.8488 23.3077C11.4468 22.8447 9.9589 22.5965 8.43031 22.5965C7.72172 22.5965 7.00417 22.6498 6.28274 22.7599C5.50254 22.879 4.85893 23.3396 4.48047 23.9669Z" fill="#000091"/>
<path d="M6.73234 27.7346C5.79697 27.7346 4.89619 27.2182 4.44789 26.324C4.06016 25.5508 4.10088 24.6744 4.48048 23.9656C4.85894 23.3383 5.50255 22.8776 6.28274 22.7586C7.00418 22.6485 7.72173 22.5952 8.43032 22.5952C9.95891 22.5952 11.4468 22.8433 12.8488 23.3064C11.5743 25.0067 9.89943 26.445 7.87638 27.462C7.50869 27.647 7.11751 27.7346 6.73234 27.7346Z" fill="#6D2700"/>
<path d="M20.3024 9.17157C20.2932 9.17157 20.2841 9.17155 20.2749 9.17148C17.7458 9.1565 15.7077 7.09128 15.7227 4.55868C15.7376 2.03535 17.7851 0 20.3013 0C20.3107 0 20.3196 4.36376e-05 20.329 8.73232e-05C22.8581 0.0150715 24.896 2.08029 24.8812 4.61289C24.8664 7.13624 22.8189 9.17157 20.3024 9.17157Z" fill="#000091"/>
<path d="M20.3304 15.5561C20.302 15.5561 20.2741 15.5561 20.2457 15.5559C18.7099 15.5467 17.1755 15.2905 15.7082 14.7877C15.7175 12.6241 15.2333 10.4269 14.1994 8.36499C13.8453 7.65864 13.2089 7.18804 12.4961 7.02148C12.996 7.11156 13.4738 7.3514 13.858 7.74064C15.6347 9.54114 17.9821 10.4433 20.3299 10.4433C22.642 10.4433 24.9547 9.56899 26.7254 7.81682C27.2232 7.32436 27.8721 7.07852 28.5206 7.07852C29.1793 7.07852 29.8376 7.33204 30.3371 7.83827C30.774 8.28092 31.0165 8.84278 31.0654 9.41786C30.9563 8.62108 30.4749 7.88805 29.7074 7.49067C29.3324 7.29651 28.9315 7.20446 28.5365 7.20446C27.6119 7.20446 26.7196 7.70883 26.2656 8.58803C25.2261 10.6017 24.7152 12.751 24.6821 14.8768C23.2695 15.3296 21.7996 15.5561 20.3304 15.5561Z" fill="#000091"/>
<path d="M24.6797 14.8774C24.7128 12.7517 25.2237 10.6024 26.2632 8.58864C26.7172 7.70945 27.6095 7.20508 28.5341 7.20508C28.9291 7.20508 29.33 7.29712 29.705 7.49128C30.4725 7.88867 30.9539 8.62169 31.063 9.41847C31.1249 10.1488 30.8748 10.9004 30.3133 11.4558C28.6746 13.0776 26.7374 14.2179 24.6797 14.8774Z" fill="#000091"/>
<path d="M15.713 14.7878C13.7042 14.0995 11.8208 12.9487 10.2296 11.3363C9.23809 10.3317 9.24766 8.71228 10.251 7.71937C10.7487 7.22682 11.3976 6.98096 12.0461 6.98096C12.1983 6.98096 12.3507 6.99452 12.5009 7.02158C13.2137 7.18814 13.8501 7.65874 14.2042 8.36509C15.2381 10.427 15.7223 12.6242 15.713 14.7878Z" fill="#000091"/>
</svg>

Before

Width:  |  Height:  |  Size: 6.9 KiB

@@ -0,0 +1,22 @@
<svg
width="48"
height="49"
viewBox="0 0 48 49"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M41.7012 0.558594C45.1791 0.558594 47.9995 3.3445 47.9995 6.91157C47.9995 16.0522 47.9995 33.1064 47.9995 42.247C47.9995 45.7026 45.1791 48.6 41.7012 48.6C32.5048 48.6 15.5055 48.6 6.30916 48.6C2.82012 48.6 0 45.7026 0 42.247C0 33.1064 0 16.0522 0 6.91157C0 3.3445 2.82012 0.558594 6.30916 0.558594H41.7012Z"
fill="white"
/>
<path
d="M35.0137 22.6992C35.6937 22.6992 36.3537 22.7792 37.0137 22.8792V13.2392L22.0137 6.69922L7.01367 13.2392V23.0592C7.01367 32.1392 13.4137 40.6392 22.0137 42.6992C23.1137 42.4392 24.1737 42.0592 25.2137 41.5992C23.8337 39.6392 23.0137 37.2592 23.0137 34.6992C23.0137 28.0792 28.3937 22.6992 35.0137 22.6992Z"
fill="#000091"
/>
<path
d="M35.0137 26.6992C30.5937 26.6992 27.0137 30.2792 27.0137 34.6992C27.0137 39.1192 30.5937 42.6992 35.0137 42.6992C39.4337 42.6992 43.0137 39.1192 43.0137 34.6992C43.0137 30.2792 39.4337 26.6992 35.0137 26.6992ZM35.0137 29.4592C36.2537 29.4592 37.2537 30.4792 37.2537 31.6992C37.2537 32.9192 36.2337 33.9392 35.0137 33.9392C33.7937 33.9392 32.7737 32.9192 32.7737 31.6992C32.7737 30.4792 33.7737 29.4592 35.0137 29.4592ZM35.0137 40.1992C33.1537 40.1992 31.5337 39.2792 30.5337 37.8592C30.6337 36.4192 33.5537 35.6992 35.0137 35.6992C36.4737 35.6992 39.3937 36.4192 39.4937 37.8592C38.4937 39.2792 36.8737 40.1992 35.0137 40.1992Z"
fill="#E1000F"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

@@ -1,51 +0,0 @@
import { DataGrid } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import { Card } from '@/components';
export function MailContent() {
const { t } = useTranslation();
const dataset = [
{
id: '1',
name: 'John Doe',
email: 'john@doe.com',
state: 'Active',
lastConnection: '2021-09-01',
},
{
id: '2',
name: 'Jane Doe',
email: 'jane@doe.com',
state: 'Inactive',
lastConnection: '2021-09-02',
},
];
return (
<Card className="m-l p-s">
<DataGrid
columns={[
{
headerName: t('Names'),
field: 'name',
},
{
field: 'email',
headerName: t('Emails'),
},
{
field: 'state',
headerName: t('State'),
},
{
field: 'lastConnection',
headerName: t('Last Connecttion'),
},
]}
rows={dataset}
/>
</Card>
);
}
@@ -1,23 +0,0 @@
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { MainLayout } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
export function MailLayout({ children }: PropsWithChildren) {
const { colorsTokens } = useCunninghamTheme();
return (
<MainLayout>
<Box $height="inherit" $direction="row">
<Box
$background={colorsTokens()['primary-bg']}
$width="100%"
$height="inherit"
>
{children}
</Box>
</Box>
</MainLayout>
);
}
@@ -1,2 +0,0 @@
export * from './MailContent';
export * from './MailLayout';
@@ -1 +0,0 @@
export * from './components/';
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import fetchMock from 'fetch-mock';
import { Role, Team } from '@/features/teams';
import { Role } from '@/features/teams';
import { AppWrapper } from '@/tests/utils';
import { MemberAction } from '../components/MemberAction';
@@ -21,11 +21,6 @@ const access: Access = {
} as any,
};
const team = {
id: '123456',
name: 'teamName',
} as Team;
describe('MemberAction', () => {
afterEach(() => {
fetchMock.restore();
@@ -33,7 +28,7 @@ describe('MemberAction', () => {
it('checks the render when owner', async () => {
render(
<MemberAction access={access} currentRole={Role.OWNER} team={team} />,
<MemberAction access={access} currentRole={Role.OWNER} teamId="123" />,
{
wrapper: AppWrapper,
},
@@ -46,7 +41,7 @@ describe('MemberAction', () => {
it('checks the render when member', () => {
render(
<MemberAction access={access} currentRole={Role.MEMBER} team={team} />,
<MemberAction access={access} currentRole={Role.MEMBER} teamId="123" />,
{
wrapper: AppWrapper,
},
@@ -59,7 +54,7 @@ describe('MemberAction', () => {
it('checks the render when admin', async () => {
render(
<MemberAction access={access} currentRole={Role.ADMIN} team={team} />,
<MemberAction access={access} currentRole={Role.ADMIN} teamId="123" />,
{
wrapper: AppWrapper,
},
@@ -75,7 +70,7 @@ describe('MemberAction', () => {
<MemberAction
access={{ ...access, role: Role.OWNER }}
currentRole={Role.ADMIN}
team={team}
teamId="123"
/>,
{
wrapper: AppWrapper,
@@ -1,6 +1,7 @@
import {
Button,
DataGrid,
Input,
SortModel,
usePagination,
} from '@openfun/cunningham-react';
@@ -17,6 +18,7 @@ import { useTeamAccesses } from '../api/';
import { PAGE_SIZE } from '../conf';
import { MemberAction } from './MemberAction';
import styled from 'styled-components';
interface MemberGridProps {
team: Team;
@@ -50,6 +52,37 @@ function formatSortModel(
return sort === 'desc' ? `-${orderingField}` : orderingField;
}
const Wrapper = styled(Card)`
.c__pagination {
width: fit-content;
margin-bottom: 1rem;
}
.c__pagination__goto {
display: none;
}
table th:first-child,
table td:first-child {
padding-right: 0;
width: 3.5rem;
}
table td:last-child {
text-align: right;
}
`;
const Actions = styled.div`
display: flex;
flex-direction: row;
justify-content: space-between;
margin-bottom: 1.25rem;
.filter-container {
width: 50%;
}
`;
export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
const [isModalMemberOpen, setIsModalMemberOpen] = useState(false);
const { t } = useTranslation();
@@ -96,7 +129,14 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
return (
<>
{currentRole !== Role.MEMBER && (
<Box className="m-b mb-s" $align="flex-end">
<Actions>
<div className="filter-container">
<Input
rightIcon={<span className="material-icons">search</span>}
label="Filtrer la liste des membres"
fullWidth
/>
</div>
<Button
aria-label={t('Add members to the team')}
style={{
@@ -108,29 +148,10 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
>
{t('Add')}
</Button>
</Box>
</Actions>
)}
<Card
className="m-b pb-s"
$overflow="auto"
$css={`
margin-top:0;
& .c__pagination__goto {
display: none;
}
& table th:first-child,
& table td:first-child {
padding-right: 0;
width: 3.5rem;
}
& table td:last-child {
text-align: right;
}
`}
aria-label={t('List members card')}
>
<Wrapper aria-label={t('List members card')}>
{error && <TextErrors causes={error.cause} />}
<DataGrid
columns={[
{
@@ -179,7 +200,7 @@ export const MemberGrid = ({ team, currentRole }: MemberGridProps) => {
onSortModelChange={setSortModel}
sortModel={sortModel}
/>
</Card>
</Wrapper>
{isModalMemberOpen && (
<ModalAddMembers
currentRole={currentRole}
@@ -128,7 +128,7 @@ export const ModalDelete = ({ access, onClose, team }: ModalDeleteProps) => {
$theme="primary"
>
<IconUser width={20} height={20} />
<Text>{access.user.name}</Text>
<Text>{access.user.name || access.user.email}</Text>
</Text>
</Box>
</Modal>
@@ -2,36 +2,42 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import IconGroup from '@/assets/icons/icon-group.svg';
import { Box } from '@/components/';
import useCunninghamTheme from '@/cunningham/useCunninghamTheme';
import MenuItem from './MenuItems';
import IconRecent from './assets/icon-clock.svg';
import IconContacts from './assets/icon-contacts.svg';
import IconMail from './assets/icon-mails.svg';
import IconSearch from './assets/icon-search.svg';
import IconFavorite from './assets/icon-stars.svg';
import styled from 'styled-components';
export const MENU_WIDTH = '62px';
const Wrapper = styled.menu`
display: flex;
flex-direction: column;
background-color: var(--bg-color);
width: ${MENU_WIDTH};
gap: 0.8rem;
`;
export const Menu = () => {
const { colorsTokens } = useCunninghamTheme();
const { t } = useTranslation();
return (
<Box
as="menu"
className="m-0 p-0"
$background={colorsTokens()['primary-800']}
$height="100%"
$justify="space-between"
<Wrapper
className="m-0 p-0 pt-l"
style={{
'--bg-color': colorsTokens()['primary-800'],
}}
>
<Box className="pt-l" $direction="column" $gap="0.8rem">
<MenuItem Icon={IconSearch} label={t('Search')} href="/" />
<MenuItem Icon={IconMail} label={t('Mails')} href="/mails" />
<MenuItem Icon={IconFavorite} label={t('Favorite')} href="/favorite" />
<MenuItem Icon={IconRecent} label={t('Recent')} href="/recent" />
<MenuItem Icon={IconContacts} label={t('Contacts')} href="/contacts" />
<MenuItem Icon={IconGroup} label={t('Groups')} href="/groups" />
</Box>
</Box>
<MenuItem Icon={IconSearch} label={t('Search')} href="/" />
<MenuItem Icon={IconFavorite} label={t('Favorite')} href="/favorite" />
<MenuItem Icon={IconRecent} label={t('Recent')} href="/recent" />
<MenuItem Icon={IconContacts} label={t('Contacts')} href="/contacts" />
<MenuItem Icon={IconGroup} label={t('Groups')} href="/groups" />
</Wrapper>
);
};
@@ -1,13 +0,0 @@
<svg viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_879_8339)">
<path
d="M18 3C9.72 3 3 9.72 3 18C3 26.28 9.72 33 18 33H25.5V30H18C11.49 30 6 24.51 6 18C6 11.49 11.49 6 18 6C24.51 6 30 11.49 30 18V20.145C30 21.33 28.935 22.5 27.75 22.5C26.565 22.5 25.5 21.33 25.5 20.145V18C25.5 13.86 22.14 10.5 18 10.5C13.86 10.5 10.5 13.86 10.5 18C10.5 22.14 13.86 25.5 18 25.5C20.07 25.5 21.96 24.66 23.31 23.295C24.285 24.63 25.965 25.5 27.75 25.5C30.705 25.5 33 23.1 33 20.145V18C33 9.72 26.28 3 18 3ZM18 22.5C15.51 22.5 13.5 20.49 13.5 18C13.5 15.51 15.51 13.5 18 13.5C20.49 13.5 22.5 15.51 22.5 18C22.5 20.49 20.49 22.5 18 22.5Z"
fill="currentColor"
/>
</g>
<defs>
<clipPath id="clip0_879_8339">
<rect width="36" height="36" fill="currentColor" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 854 B

@@ -2,6 +2,7 @@ import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import IconGroup from '@/assets/icons/icon-group2.svg';
import { Box, Card, StyledLink, Text } from '@/components';
@@ -11,6 +12,18 @@ import { useCreateTeam } from '../api';
import { InputTeamName } from './InputTeamName';
const Wrapper = styled(Card)`
height: 70%;
padding: 2.5rem;
display: flex;
flex-direction: column;
justify-content: space-between;
width: 100%;
max-width: 24rem;
min-width: 22rem;
margin: auto;
`;
export const CardCreateTeam = () => {
const { t } = useTranslation();
const router = useRouter();
@@ -28,15 +41,7 @@ export const CardCreateTeam = () => {
const { colorsTokens } = useCunninghamTheme();
return (
<Card
className="p-b"
$height="70%"
$justify="space-between"
$width="100%"
$maxWidth="24rem"
$minWidth="22rem"
aria-label={t('Create new team card')}
>
<Wrapper aria-label={t('Create new team card')}>
<Box $gap="1rem">
<Box $align="center">
<IconGroup
@@ -64,6 +69,6 @@ export const CardCreateTeam = () => {
{t('Create the team')}
</Button>
</Box>
</Card>
</Wrapper>
);
};
@@ -60,11 +60,12 @@ export const Panel = () => {
`}
>
<Box
className="pr-l pl-s pt-s pb-s"
className="pl-s pt-s pb-s"
$direction="row"
$align="center"
$justify="space-between"
$css={`
padding-right: 2.4rem;
border-bottom: 1px solid ${colorsTokens()['primary-300']};
`}
>
@@ -63,7 +63,12 @@ export const TeamItem = ({ team }: TeamProps) => {
`}
>
<StyledLink className="p-s pt-t pb-t" href={`/teams/${team.id}`}>
<Box $align="center" $direction="row" $gap="0.5rem">
<Box
$align="center"
$direction="row"
$gap="0.5rem"
$css="min-width: 0;"
>
{hasMembers ? (
<IconGroup
aria-label={t(`Teams icon`)}
@@ -89,7 +94,10 @@ export const TeamItem = ({ team }: TeamProps) => {
$weight="bold"
$color={!hasMembers ? colorsTokens()['greyscale-600'] : undefined}
$css={`
min-width: 14rem;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`}
>
{team.name}
@@ -10,6 +10,7 @@ import { Role, Team } from '../types';
import { ModalUpdateTeam } from './ModalUpdateTeam';
import { TeamActions } from './TeamActions';
import styled from 'styled-components';
const format: DateTimeFormatOptions = {
month: '2-digit',
@@ -17,6 +18,71 @@ const format: DateTimeFormatOptions = {
year: 'numeric',
};
const Wrapper = styled(Card)`
min-height: fit-content;
.actions-container {
align-self: flex-end;
position: absolute;
right: 0;
}
margin-bottom: 3rem;
`;
const Header = styled.div`
display: flex;
flex-direction: row;
justify-content: start;
gap: 2rem;
margin: 3rem 1.25rem 2rem 3rem;
div {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
p,
h3 {
margin: 0;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
h3 {
font-weight: bold;
font-size: 1.5rem;
}
p {
margin-top: 0.625rem;
font-size: 0.81rem;
color: #303c4b;
}
}
`;
const SubHeader = styled.div`
gap: 1rem 2rem;
display: flex;
flex-direction: row;
justify-content: start;
border-top: 1px solid var(--border-color);
padding: 1rem 0 1.5rem 8rem;
flex-wrap: wrap;
overflow: hidden;
p {
display: inline;
font-size: 0.81rem;
margin: 0;
}
`;
interface TeamInfoProps {
team: Team;
currentRole: Role;
@@ -38,13 +104,13 @@ export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
return (
<>
<Card className="m-b" style={{ paddingBottom: 0 }}>
<Box $css="align-self: flex-end;" className="m-t" $position="absolute">
<Wrapper>
<div className="actions-container m-t">
<TeamActions currentRole={currentRole} team={team} />
</Box>
<Box className="m-b" $direction="row" $align="center" $gap="1.5rem">
</div>
<Header>
<IconGroup
width={44}
width={48}
color={colorsTokens()['primary-text']}
aria-label={t('icon group')}
style={{
@@ -52,46 +118,35 @@ export const TeamInfo = ({ team, currentRole }: TeamInfoProps) => {
alignSelf: 'start',
}}
/>
<Box>
<Text as="h3" $weight="bold" $size="1.25rem" className="mt-0">
<div>
<h3>
{t('Members of “{{teamName}}“', {
teamName: team.name,
})}
</Text>
<Text $size="m">
</h3>
<p>
{t('Add people to the “{{teamName}}“ group.', {
teamName: team.name,
})}
</Text>
</Box>
</Box>
<Box
className="p-s"
$gap="3rem"
$direction="row"
$justify="start"
$css={`
border-top: 1px solid ${colorsTokens()['card-border']};
padding-left: 1.5rem;
`}
</p>
</div>
</Header>
<SubHeader
style={{
'--border-color': colorsTokens()['card-border'],
}}
>
<Text $size="s" as="p">
{t('{{count}} member', { count: team.accesses.length })}
</Text>
<Text $size="s" $display="inline" as="p">
<p>{t('{{count}} member', { count: team.accesses.length })}</p>
<p>
{t('Created at')}&nbsp;
<Text $weight="bold" $display="inline">
{created_at}
</Text>
</Text>
<Text $size="s" $display="inline" as="p">
<strong>{created_at}</strong>
</p>
<p>
{t('Last update at')}&nbsp;
<Text $weight="bold" $display="inline">
{updated_at}
</Text>
</Text>
</Box>
</Card>
<strong>{updated_at}</strong>
</p>
</SubHeader>
</Wrapper>
{isModalUpdateOpen && (
<ModalUpdateTeam
onClose={() => setIsModalUpdateOpen(false)}
@@ -1,25 +1,34 @@
import { PropsWithChildren } from 'react';
import { Box } from '@/components';
import { MainLayout } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Panel } from '@/features/teams';
import styled from 'styled-components';
const Container = styled.div`
display: flex;
flex-direction: column;
background-color: var(--bg-color);
overflow-x: hidden;
overflow-y: overlay;
width: 100%;
height: 100%;
padding: 1.85rem;
`;
export function TeamLayout({ children }: PropsWithChildren) {
const { colorsTokens } = useCunninghamTheme();
return (
<MainLayout>
<Box $height="inherit" $direction="row">
<Panel />
<Box
$background={colorsTokens()['primary-bg']}
$width="100%"
$height="inherit"
>
{children}
</Box>
</Box>
<Panel />
<Container
style={{
'--bg-color': colorsTokens()['primary-bg'],
}}
>
{children}
</Container>
</MainLayout>
);
}
@@ -36,13 +36,11 @@
"Created at": "Créé le",
"Delete the team": "Supprimer le groupe",
"Deleting the {{teamName}} team": "Suppression du groupe {{teamName}}",
"Desk": "Desk",
"Desk Logo": "Logo Desk",
"Emails": "Emails",
"Empty teams icon": "Icône de groupe vide",
"Enter the new name of the selected team": "Entrez le nouveau nom du groupe sélectionné",
"Equipes": "Equipes",
"Equipes Description": "Description de Equipes",
"Equipes Logo": "Equipes Logo",
"Error occurred while logging out": "La déconnexion a échoué",
"Failed to add {{name}} in the team": "Impossible d'ajouter {{name}} au groupe",
"Failed to create the invitation for {{email}}": "Impossible de créer l'invitation pour {{email}}",
"Favorite": "Favoris",
@@ -50,19 +48,17 @@
"Freedom Equality Fraternity Logo": "Logo Liberté Égalité Fraternité",
"Groups": "Groupes",
"Invitation sent to {{email}}": "Invitation envoyée à {{email}}",
"Invite new members to {{teamName}}": "Invitez de nouveaux membres à rejoindre {{teamName}}",
"Invite new members to your team": "Invitez de nouveaux membres à rejoindre votre groupe",
"It seems that the page you are looking for does not exist or cannot be displayed correctly.": "Il semble que la page que vous cherchez n'existe pas ou ne puisse pas être affichée correctement.",
"Language": "Langue",
"Language Icon": "Icône de langue",
"Last update at": "Dernière modification le",
"List members card": "Carte liste des membres",
"Logout": "Déconnexion",
"Marianne Logo": "Logo Marianne",
"Member": "Membre",
"Member icon": "Icône de membre",
"Member {{name}} added to the team": "Membre {{name}} ajouté au groupe",
"Members of “{{teamName}}“": "Membres de “{{teamName}}“",
"My account": "Mon compte",
"Name the team": "Nommer le groupe",
"Names": "Noms",
"New name...": "Nouveau nom...",
@@ -71,6 +67,8 @@
"Open the team options": "Ouvrir les options de groupe",
"Ouch !": "Ooops !",
"Owner": "Propriétaire",
"People": "People",
"People Description": "Description de People",
"Profile picture": "Photo du profil",
"Radio buttons to update the roles": "Boutons radio pour mettre à jour les rôles",
"Recent": "Récent",
+2 -2
View File
@@ -18,8 +18,8 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
return (
<>
<Head>
<title>{t('Equipes')}</title>
<meta name="description" content={t('Equipes Description')} />
<title>{t('People')}</title>
<meta name="description" content={t('People Description')} />
<link rel="icon" href="/favicon.ico" sizes="any" />
</Head>
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
@@ -1,14 +0,0 @@
import { ReactElement } from 'react';
import { MailContent, MailLayout } from '@/features/mails';
import { NextPageWithLayout } from '@/types/next';
const Page: NextPageWithLayout = () => {
return <MailContent />;
};
Page.getLayout = function getLayout(page: ReactElement) {
return <MailLayout>{page}</MailLayout>;
};
export default Page;
@@ -3,10 +3,18 @@ import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, StyledLink } from '@/components';
import { StyledLink } from '@/components';
import { TeamLayout } from '@/features/teams/';
import { NextPageWithLayout } from '@/types/next';
const Container = styled.div`
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
`;
const StyledButton = styled(Button)`
width: fit-content;
`;
@@ -15,11 +23,11 @@ const Page: NextPageWithLayout = () => {
const { t } = useTranslation();
return (
<Box $align="center" $justify="center" $height="inherit">
<Container>
<StyledLink href="/teams/create">
<StyledButton>{t('Create a new team')}</StyledButton>
</StyledLink>
</Box>
</Container>
);
};
@@ -39,7 +39,9 @@ export const createTeam = async (
await page.getByText('Team name').fill(randomTeams[i]);
await expect(buttonCreate).toBeEnabled();
await buttonCreate.click();
await expect(panel.locator('li').getByText(randomTeams[i])).toBeVisible();
await expect(
panel.locator('li').nth(0).getByText(randomTeams[i]),
).toBeVisible();
}
return randomTeams;
@@ -17,12 +17,12 @@ test.describe('Header', () => {
header.getByAltText('Freedom Equality Fraternity Logo'),
).toBeVisible();
await expect(header.getByAltText('Equipes Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Equipes')).toHaveCSS(
await expect(header.getByAltText('Desk Logo')).toBeVisible();
await expect(header.locator('h2').getByText('Desk')).toHaveCSS(
'color',
'rgb(0, 0, 145)',
);
await expect(header.locator('h2').getByText('Equipes')).toHaveCSS(
await expect(header.locator('h2').getByText('Desk')).toHaveCSS(
'font-family',
/Marianne/i,
);
@@ -32,24 +32,12 @@ test.describe('Header', () => {
).toBeVisible();
await expect(header.getByAltText('Language Icon')).toBeVisible();
await expect(header.getByText('My account')).toBeVisible();
});
test('checks logout button', async ({ page }) => {
await page
.getByRole('button', {
name: 'My account',
})
.click();
await page
.getByRole('button', {
name: 'Logout',
})
.click();
// FIXME - assert the session has been killed in Keycloak
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
await expect(header.getByText('John Doe')).toBeVisible();
await expect(
header.getByRole('img', {
name: 'profile picture',
}),
).toBeVisible();
});
});
@@ -1,16 +0,0 @@
import { expect, test } from '@playwright/test';
import { keyCloakSignIn } from './common';
test.beforeEach(async ({ page, browserName }) => {
await page.goto('/');
await keyCloakSignIn(page, browserName);
});
test.describe('Mails', () => {
test('checks all the elements are visible', async ({ page }) => {
await page.locator('menu').first().getByLabel(`Mails button`).click();
await expect(page.getByText('john@doe.com')).toBeVisible();
await expect(page.getByText('jane@doe.com')).toBeVisible();
});
});
@@ -49,7 +49,7 @@ test.describe('Members Create', () => {
await page.getByRole('option', { name: users[0].name }).click();
// Select user 2
await inputSearch.fill('test');
await inputSearch.fill('test1');
await page.getByRole('option', { name: users[1].name }).click();
// Select email
@@ -10,7 +10,6 @@ test.beforeEach(async ({ page, browserName }) => {
test.describe('Menu', () => {
const menuItems = [
{ name: 'Search', isDefault: true },
{ name: 'Mails', isDefault: false },
{ name: 'Favorite', isDefault: false },
{ name: 'Recent', isDefault: false },
{ name: 'Contacts', isDefault: false },
+1 -1
View File
@@ -9,7 +9,7 @@
"test:ui": "yarn test --ui"
},
"devDependencies": {
"@playwright/test": "1.43.0",
"@playwright/test": "1.42.1",
"@types/node": "*",
"eslint-config-people": "*",
"typescript": "*"
+3 -3
View File
@@ -24,8 +24,8 @@
"i18n:test": "yarn I18N run test"
},
"resolutions": {
"@types/node": "20.12.5",
"@types/react-dom": "18.2.24",
"typescript": "5.4.4"
"@types/node": "20.12.2",
"@types/react-dom": "18.2.23",
"typescript": "5.4.3"
}
}
@@ -7,13 +7,13 @@
},
"dependencies": {
"@next/eslint-plugin-next": "14.1.4",
"@tanstack/eslint-plugin-query": "5.28.11",
"@tanstack/eslint-plugin-query": "5.28.6",
"@typescript-eslint/eslint-plugin": "7.5.0",
"eslint": "8.57.0",
"eslint-config-next": "14.1.4",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "28.2.0",
"eslint-plugin-jest": "27.9.0",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-playwright": "1.5.4",
"eslint-plugin-prettier": "5.1.3",
+108 -117
View File
@@ -28,23 +28,23 @@
"@babel/highlight" "^7.24.2"
picocolors "^1.0.0"
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.4.tgz#6f102372e9094f25d908ca0d34fc74c74606059a"
integrity sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.24.1.tgz#31c1f66435f2a9c329bb5716a6d6186c516c3742"
integrity sha512-Pc65opHDliVpRHuKfzI+gSA4zcgr65O4cl64fFJIWEEh8JoHIHh0Oez1Eo8Arz8zq/JhgKodQaxEwUPRtZylVA==
"@babel/core@^7.0.0", "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.21.3", "@babel/core@^7.23.9":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717"
integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==
version "7.24.3"
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.3.tgz#568864247ea10fbd4eff04dda1e05f9e2ea985c3"
integrity sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==
dependencies:
"@ampproject/remapping" "^2.2.0"
"@babel/code-frame" "^7.24.2"
"@babel/generator" "^7.24.4"
"@babel/generator" "^7.24.1"
"@babel/helper-compilation-targets" "^7.23.6"
"@babel/helper-module-transforms" "^7.23.3"
"@babel/helpers" "^7.24.4"
"@babel/parser" "^7.24.4"
"@babel/helpers" "^7.24.1"
"@babel/parser" "^7.24.1"
"@babel/template" "^7.24.0"
"@babel/traverse" "^7.24.1"
"@babel/types" "^7.24.0"
@@ -54,10 +54,10 @@
json5 "^2.2.3"
semver "^6.3.1"
"@babel/generator@^7.24.1", "@babel/generator@^7.24.4", "@babel/generator@^7.7.2":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.4.tgz#1fc55532b88adf952025d5d2d1e71f946cb1c498"
integrity sha512-Xd6+v6SnjWVx/nus+y0l1sxMOTOMBkyL4+BIdbALyatQnAe/SRVjANeDPSCYaX+i1iJmuGSKf3Z+E+V/va1Hvw==
"@babel/generator@^7.24.1", "@babel/generator@^7.7.2":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.24.1.tgz#e67e06f68568a4ebf194d1c6014235344f0476d0"
integrity sha512-DfCRfZsBcrPEHUfuBMgbJ1Ut01Y/itOs+hY2nFLgqsqXd52/iSiVq5TITtUasIUgm+IIKdY2/1I7auiQOEeC9A==
dependencies:
"@babel/types" "^7.24.0"
"@jridgewell/gen-mapping" "^0.3.5"
@@ -89,10 +89,10 @@
lru-cache "^5.1.1"
semver "^6.3.1"
"@babel/helper-create-class-features-plugin@^7.24.1", "@babel/helper-create-class-features-plugin@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.4.tgz#c806f73788a6800a5cfbbc04d2df7ee4d927cce3"
integrity sha512-lG75yeuUSVu0pIcbhiYMXBXANHrpUPaOfu7ryAzskCgKUHuAxRQI5ssrtmF0X9UXldPlvT0XM/A4F44OXRt6iQ==
"@babel/helper-create-class-features-plugin@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.1.tgz#db58bf57137b623b916e24874ab7188d93d7f68f"
integrity sha512-1yJa9dX9g//V6fDebXoEfEsxkZHk3Hcbm+zLhyu6qVgYFLvmTALTeV+jNU9e5RnYtioBrGEOdoI2joMSNQ/+aA==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-environment-visitor" "^7.22.20"
@@ -244,10 +244,10 @@
"@babel/template" "^7.22.15"
"@babel/types" "^7.22.19"
"@babel/helpers@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.4.tgz#dc00907fd0d95da74563c142ef4cd21f2cb856b6"
integrity sha512-FewdlZbSiwaVGlgT1DPANDuCHaDMiOo+D/IDYRFYjHOuv66xMSJ7fQwwODwRNAPkADIO/z1EoF/l2BCWlWABDw==
"@babel/helpers@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.1.tgz#183e44714b9eba36c3038e442516587b1e0a1a94"
integrity sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==
dependencies:
"@babel/template" "^7.24.0"
"@babel/traverse" "^7.24.1"
@@ -263,18 +263,10 @@
js-tokens "^4.0.0"
picocolors "^1.0.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.24.0", "@babel/parser@^7.24.1", "@babel/parser@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.4.tgz#234487a110d89ad5a3ed4a8a566c36b9453e8c88"
integrity sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==
"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.4.tgz#6125f0158543fb4edf1c22f322f3db67f21cb3e1"
integrity sha512-qpl6vOOEEzTLLcsuqYYo8yDtrTocmu2xkGvgNebvPjT9DTtfFYGmgDqY+rBYXNlqL4s9qLDn6xkrJv4RxAPiTA==
dependencies:
"@babel/helper-environment-visitor" "^7.22.20"
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.7", "@babel/parser@^7.23.9", "@babel/parser@^7.24.0", "@babel/parser@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.1.tgz#1e416d3627393fab1cb5b0f2f1796a100ae9133a"
integrity sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1":
version "7.24.1"
@@ -486,10 +478,10 @@
dependencies:
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-transform-block-scoping@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.4.tgz#28f5c010b66fbb8ccdeef853bef1935c434d7012"
integrity sha512-nIFUZIpGKDf9O9ttyRXpHFpKC+X3Y5mtshZONuEUYBomAKoM4y029Jr+uB1bHGPhNmK8YXHevDtKDOLmtRrp6g==
"@babel/plugin-transform-block-scoping@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.1.tgz#27af183d7f6dad890531256c7a45019df768ac1f"
integrity sha512-h71T2QQvDgM2SmT29UYU6ozjMlAt7s7CSs5Hvy8f8cf/GM/Z4a2zMfN+fjVGaieeCrXR3EdQl6C4gQG+OgmbKw==
dependencies:
"@babel/helper-plugin-utils" "^7.24.0"
@@ -501,12 +493,12 @@
"@babel/helper-create-class-features-plugin" "^7.24.1"
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-transform-class-static-block@^7.24.4":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz#1a4653c0cf8ac46441ec406dece6e9bc590356a4"
integrity sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==
"@babel/plugin-transform-class-static-block@^7.24.1":
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.1.tgz#4e37efcca1d9f2fcb908d1bae8b56b4b6e9e1cb6"
integrity sha512-FUHlKCn6J3ERiu8Dv+4eoz7w8+kFLSyeVG4vDAikwADGjUCoHw/JHokyGtr8OR4UjpwPVivyF+h8Q5iv/JmrtA==
dependencies:
"@babel/helper-create-class-features-plugin" "^7.24.4"
"@babel/helper-create-class-features-plugin" "^7.24.1"
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-syntax-class-static-block" "^7.14.5"
@@ -850,12 +842,12 @@
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-transform-typescript@^7.24.1":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.4.tgz#03e0492537a4b953e491f53f2bc88245574ebd15"
integrity sha512-79t3CQ8+oBGk/80SQ8MN3Bs3obf83zJ0YZjDmDaEZN8MqhMI760apl5z6a20kFeMXBwJX99VpKT8CKxEBp5H1g==
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.1.tgz#5c05e28bb76c7dfe7d6c5bed9951324fd2d3ab07"
integrity sha512-liYSESjX2fZ7JyBFkYG78nfvHlMKE6IpNdTVnxmlYUR+j5ZLsitFbaAE+eJSK2zPPkNWNw4mXL51rQ8WrvdK0w==
dependencies:
"@babel/helper-annotate-as-pure" "^7.22.5"
"@babel/helper-create-class-features-plugin" "^7.24.4"
"@babel/helper-create-class-features-plugin" "^7.24.1"
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/plugin-syntax-typescript" "^7.24.1"
@@ -891,15 +883,14 @@
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/preset-env@^7.20.2":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.4.tgz#46dbbcd608771373b88f956ffb67d471dce0d23b"
integrity sha512-7Kl6cSmYkak0FK/FXjSEnLJ1N9T/WA2RkMhu17gZ/dsxKJUuTYNIylahPTzqpLyJN4WhDif8X0XK1R8Wsguo/A==
version "7.24.3"
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.3.tgz#f3f138c844ffeeac372597b29c51b5259e8323a3"
integrity sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==
dependencies:
"@babel/compat-data" "^7.24.4"
"@babel/compat-data" "^7.24.1"
"@babel/helper-compilation-targets" "^7.23.6"
"@babel/helper-plugin-utils" "^7.24.0"
"@babel/helper-validator-option" "^7.23.5"
"@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.24.4"
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.24.1"
"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.24.1"
"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.24.1"
@@ -926,9 +917,9 @@
"@babel/plugin-transform-async-generator-functions" "^7.24.3"
"@babel/plugin-transform-async-to-generator" "^7.24.1"
"@babel/plugin-transform-block-scoped-functions" "^7.24.1"
"@babel/plugin-transform-block-scoping" "^7.24.4"
"@babel/plugin-transform-block-scoping" "^7.24.1"
"@babel/plugin-transform-class-properties" "^7.24.1"
"@babel/plugin-transform-class-static-block" "^7.24.4"
"@babel/plugin-transform-class-static-block" "^7.24.1"
"@babel/plugin-transform-classes" "^7.24.1"
"@babel/plugin-transform-computed-properties" "^7.24.1"
"@babel/plugin-transform-destructuring" "^7.24.1"
@@ -1015,9 +1006,9 @@
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.0.0", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.6", "@babel/runtime@^7.22.15", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.9", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
version "7.24.4"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
version "7.24.1"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57"
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
dependencies:
regenerator-runtime "^0.14.0"
@@ -1839,12 +1830,12 @@
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
"@playwright/test@1.43.0":
version "1.43.0"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.43.0.tgz#5d90f247b26d404dd5d81c60f9c7c5e5159eb664"
integrity sha512-Ebw0+MCqoYflop7wVKj711ccbNlrwTBCtjY5rlbiY9kHL2bCYxq+qltK6uPsVBGGAOb033H2VO0YobcQVxoW7Q==
"@playwright/test@1.42.1":
version "1.42.1"
resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.42.1.tgz#9eff7417bcaa770e9e9a00439e078284b301f31c"
integrity sha512-Gq9rmS54mjBL/7/MvBaNOBwbfnh7beHvS6oS4srqXFcQHpQCV1+c8JXWE8VLPyRDhgS3H8x8A7hztqI9VnwrAQ==
dependencies:
playwright "1.43.0"
playwright "1.42.1"
"@react-aria/breadcrumbs@^3.5.11":
version "3.5.11"
@@ -3023,36 +3014,36 @@
dependencies:
tslib "^2.4.0"
"@tanstack/eslint-plugin-query@5.28.11":
version "5.28.11"
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.28.11.tgz#0a46f2b301b706d993fc4bc25fef60c1fb639101"
integrity sha512-bODGLeG4WCGmHVKCh3bH1KLfq7xdi1jsRjTESV6ifCw1mZ0m2fBMxAjK42KjbhJwcvNdTlYHI+YY/aZWBk4Niw==
"@tanstack/eslint-plugin-query@5.28.6":
version "5.28.6"
resolved "https://registry.yarnpkg.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.28.6.tgz#55eef14ab75bc2a3685e8104657c3da009ce2644"
integrity sha512-kIvdN/EvbOrk4bbXOBm/Ik+uhQl5hawikkF5dLLmlvK3aZJzwRaRGpezYDM5Xw/6GCsATy+woh+Wvzj//BRvsg==
dependencies:
"@typescript-eslint/utils" "^6.20.0"
"@tanstack/query-core@5.29.0":
version "5.29.0"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.29.0.tgz#d0b3d12c07d5a47f42ab0c1ed4f317106f3d4b20"
integrity sha512-WgPTRs58hm9CMzEr5jpISe8HXa3qKQ8CxewdYZeVnA54JrPY9B1CZiwsCoLpLkf0dGRZq+LcX5OiJb0bEsOFww==
"@tanstack/query-core@5.28.9":
version "5.28.9"
resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.28.9.tgz#170a7a8794ab73aeffbaf711ac62126479a5d026"
integrity sha512-hNlfCiqZevr3GRVPXS3MhaGW5hjcxvCsIQ4q6ff7EPlvFwYZaS+0d9EIIgofnegDaU2BbCDlyURoYfRl5rmzow==
"@tanstack/query-devtools@5.28.10":
version "5.28.10"
resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.28.10.tgz#33e9a42dd2199fca12f0dd2d891570ecdbfd3c7b"
integrity sha512-5UN629fKa5/1K/2Pd26gaU7epxRrYiT1gy+V+pW5K6hnf1DeUKK3pANSb2eHKlecjIKIhTwyF7k9XdyE2gREvQ==
"@tanstack/react-query-devtools@5.29.0":
version "5.29.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.29.0.tgz#fde50304cc777c9bc8ad3f2f8afcd62412636984"
integrity sha512-WLuaU6yM4KdvBimEP1Km5lM4/p1J40cMp5I5z0Mc6a8QbBUYLK8qJcGIKelfLfDp7KmEcr59tzbRTmdH/GWvzQ==
"@tanstack/react-query-devtools@5.28.10":
version "5.28.10"
resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.28.10.tgz#0b968b93e301a696d06dc44198d39aac95e8c5b9"
integrity sha512-D+SiHZTWhK2sNgBYj+xIvUOqonsKy74OLU/YHmRB5OZVLLTiekvZd12C3rKlU+WM69jid0hjEjuFqkULOMwc3A==
dependencies:
"@tanstack/query-devtools" "5.28.10"
"@tanstack/react-query@5.29.0":
version "5.29.0"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.29.0.tgz#42b3a2de4ed1d63666f0af04392a34b5e70d49c0"
integrity sha512-yxlhHB73jaBla6h5B6zPaGmQjokkzAhMHN4veotkPNiQ3Ac/mCxgABRZPsJJrgCTvhpcncBZcDBFxaR2B37vug==
"@tanstack/react-query@5.28.9":
version "5.28.9"
resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.28.9.tgz#13c2049daa5db6c3137473e279b209f76d39708e"
integrity sha512-vwifBkGXsydsLxFOBMe3+f8kvtDoqDRDwUNjPHVDDt+FoBetCbOWAUHgZn4k+CVeZgLmy7bx6aKeDbe3e8koOQ==
dependencies:
"@tanstack/query-core" "5.29.0"
"@tanstack/query-core" "5.28.9"
"@tanstack/react-table@8.13.2":
version "8.13.2"
@@ -3244,10 +3235,10 @@
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
integrity sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==
"@types/node@*", "@types/node@20.12.5":
version "20.12.5"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3"
integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==
"@types/node@*", "@types/node@20.12.2":
version "20.12.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.2.tgz#9facdd11102f38b21b4ebedd9d7999663343d72e"
integrity sha512-zQ0NYO87hyN6Xrclcqp7f8ZbXNbRfoGWNcMvHTPQp9UUrwI0mI7XBz+cu7/W6/VClYo2g63B0cjull/srU7LgQ==
dependencies:
undici-types "~5.26.4"
@@ -3261,10 +3252,10 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/react-dom@*", "@types/react-dom@18.2.24", "@types/react-dom@^18.0.0":
version "18.2.24"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.24.tgz#8dda8f449ae436a7a6e91efed8035d4ab03ff759"
integrity sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==
"@types/react-dom@*", "@types/react-dom@18.2.23", "@types/react-dom@^18.0.0":
version "18.2.23"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.23.tgz#112338760f622a16d64271b408355f2f27f6302c"
integrity sha512-ZQ71wgGOTmDYpnav2knkjr3qXdAFu0vsk8Ci5w3pGAIdj7/kKAyn+VsQDhXsmzzzepAiI9leWMmubXz690AI/A==
dependencies:
"@types/react" "*"
@@ -3275,10 +3266,10 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.74":
version "18.2.74"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.74.tgz#2d52eb80e4e7c4ea8812c89181d6d590b53f958c"
integrity sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==
"@types/react@*", "@types/react@18.2.73":
version "18.2.73"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.73.tgz#0579548ad122660d99e00499d22e33b81e73ed94"
integrity sha512-XcGdod0Jjv84HOC7N5ziY3x+qL0AfmubvKOZ9hJjJ2yd5EE+KYjWhdOjt387e9HPheHkdggF9atTifMRtyAaRA==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
@@ -3451,7 +3442,7 @@
"@typescript-eslint/typescript-estree" "7.5.0"
semver "^7.5.4"
"@typescript-eslint/utils@^5.58.0":
"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.0":
version "5.62.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.62.0.tgz#141e809c71636e4a75daa39faed2fb5f4b10df86"
integrity sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==
@@ -3465,7 +3456,7 @@
eslint-scope "^5.1.1"
semver "^7.3.7"
"@typescript-eslint/utils@^6.0.0", "@typescript-eslint/utils@^6.20.0":
"@typescript-eslint/utils@^6.20.0":
version "6.21.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-6.21.0.tgz#4714e7a6b39e773c1c8e97ec587f520840cd8134"
integrity sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==
@@ -4027,9 +4018,9 @@ camelize@^1.0.0:
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
version "1.0.30001607"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001607.tgz#b91e8e033f6bca4e13d3d45388d87fa88931d9a5"
integrity sha512-WcvhVRjXLKFB/kmOFVwELtMxyhq3iM/MvmXcyCe2PNf166c39mptscOc/45TTS96n2gpNV2z7+NakArTWZCQ3w==
version "1.0.30001605"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz#ca12d7330dd8bcb784557eb9aa64f0037870d9d6"
integrity sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0:
version "4.1.2"
@@ -4675,9 +4666,9 @@ eastasianwidth@^0.2.0:
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
electron-to-chromium@^1.4.668:
version "1.4.729"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00"
integrity sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==
version "1.4.723"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.723.tgz#827da30c96b316684d352c3d81430029df01bb8e"
integrity sha512-rxFVtrMGMFROr4qqU6n95rUi9IlfIm+lIAt+hOToy/9r6CDv0XiEcQdC3VP71y1pE5CFTzKV0RvxOGYCPWWHPw==
emittery@^0.13.1:
version "0.13.1"
@@ -4999,12 +4990,12 @@ eslint-plugin-import@2.29.1, eslint-plugin-import@^2.28.1:
semver "^6.3.1"
tsconfig-paths "^3.15.0"
eslint-plugin-jest@28.2.0:
version "28.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-28.2.0.tgz#863e2b2bda95eb41981ba9bcf4c44f57dce40a73"
integrity sha512-yRDti/a+f+SMSmNTiT9/M/MzXGkitl8CfzUxnpoQcTyfq8gUrXMriVcWU36W1X6BZSUoyUCJrDAWWUA2N4hE5g==
eslint-plugin-jest@27.9.0:
version "27.9.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz#7c98a33605e1d8b8442ace092b60e9919730000b"
integrity sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==
dependencies:
"@typescript-eslint/utils" "^6.0.0"
"@typescript-eslint/utils" "^5.10.0"
eslint-plugin-jsx-a11y@6.8.0, eslint-plugin-jsx-a11y@^6.7.1:
version "6.8.0"
@@ -7421,17 +7412,17 @@ pkg-dir@^4.2.0:
dependencies:
find-up "^4.0.0"
playwright-core@1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.43.0.tgz#d8079acb653abebb0b63062e432479647a4e1271"
integrity sha512-iWFjyBUH97+pUFiyTqSLd8cDMMOS0r2ZYz2qEsPjH8/bX++sbIJT35MSwKnp1r/OQBAqC5XO99xFbJ9XClhf4w==
playwright-core@1.42.1:
version "1.42.1"
resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.42.1.tgz#13c150b93c940a3280ab1d3fbc945bc855c9459e"
integrity sha512-mxz6zclokgrke9p1vtdy/COWBH+eOZgYUVVU34C73M+4j4HLlQJHtfcqiqqxpP0o8HhMkflvfbquLX5dg6wlfA==
playwright@1.43.0:
version "1.43.0"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.43.0.tgz#2c2efd4ee2a25defd8c24c98ccb342bdd9d435f5"
integrity sha512-SiOKHbVjTSf6wHuGCbqrEyzlm6qvXcv7mENP+OZon1I07brfZLGdfWV0l/efAzVx7TF3Z45ov1gPEkku9q25YQ==
playwright@1.42.1:
version "1.42.1"
resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.42.1.tgz#79c828b51fe3830211137550542426111dc8239f"
integrity sha512-PgwB03s2DZBcNRoW+1w9E+VkLBxweib6KTXM0M3tkiT4jVxKSi6PmVJ591J+0u10LUrgxB7dLRbiJqO5s2QPMg==
dependencies:
playwright-core "1.43.0"
playwright-core "1.42.1"
optionalDependencies:
fsevents "2.3.2"
@@ -8747,10 +8738,10 @@ typed-array-length@^1.0.6:
is-typed-array "^1.1.13"
possible-typed-array-names "^1.0.0"
typescript@*, typescript@5.4.4, typescript@^5.0.4:
version "5.4.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952"
integrity sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==
typescript@*, typescript@5.4.3, typescript@^5.0.4:
version "5.4.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.3.tgz#5c6fedd4c87bee01cd7a528a30145521f8e0feff"
integrity sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==
unbox-primitive@^1.0.2:
version "1.0.2"
+36 -36
View File
@@ -1,7 +1,7 @@
djangoSecretKey: ENC[AES256_GCM,data:06KBEHV/gBgGoB4DXf9yTU5XK1xP9OXfyKEiSdSghV8XIMon3o1ajSWN+WNMRHkRZuU=,iv:ZeP1X4pQF9fVm7quzzVXSm2CSLrqAizwZD5QFmNOoSc=,tag:Dm/b+6CfznSC+CdKj1SCYA==,type:str]
djangoSecretKey: ENC[AES256_GCM,data:XKgM2zd1+/bxvdUzBp3zK0XUPDPjDwsfDa6WPVUULqxZ0RcuPsKRMjBgFvE9hLd4AvY=,iv:0Uk4QXWiAW6HW/7kmx2hbwU3sEdjIsTxm3T5U0wlbws=,tag:9I+fEYxkLClZ39x1eNP7sQ==,type:str]
oidc:
clientId: ENC[AES256_GCM,data:SZVk5bazY22AptGdO1dIalUk46nmA8fA0ggjOZKSCVrFARUh,iv:tXQ2FHOt5xCq2bV9L2iKcLQImsAiPQdU08va6UOpQj4=,tag:T5e9f7u51xxJXHpcLiAYFQ==,type:str]
clientSecret: ENC[AES256_GCM,data:xwecsL1rRF7b5rmRB9Eg1xQ/QevkD1vJPgOI55oB1bmCjP/2/q7JV5EURvxjWXFzY0mppLv9pWrxGIR8fJH1bQ==,iv:JypgxBJye0zqTJN5m9YmZT/OWG3m4Eu8dgplw2mCnCs=,tag:prLdglhObvRbSzBNqaF4Mg==,type:str]
clientId: ENC[AES256_GCM,data:KlkyIG8tNj6Nj3G4nIN+QGt9FPtMIkoitC8jxx5n4hHq71mF,iv:AKrdqPnBFLNxtRB1cphRKtH9ccwx7V4ApspjIQxtWmY=,tag:8Upvn77PKsJ0ktQh/orXqQ==,type:str]
clientSecret: ENC[AES256_GCM,data:O6RwyuiaXGO3afc4sRQz5nHW62Dkx2/I4jVqGgkms/fsDHpCMs0I3iTfGPUgI4uER60Yml16yc6n/7LWbqoy+A==,iv:1wJhrsNOZcgduy4N5WNuUPNX2R5fwyMJTpjV8IPm7Hc=,tag:Q42WTMFIPSdEtllHyLZwbQ==,type:str]
sops:
kms: []
gcp_kms: []
@@ -11,59 +11,59 @@ sops:
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArdENIcUkwdjFsQlJubW0z
Y2dPeStzVnRjcVlPcjJLQlFIdjBQRnVFcFJRCmU5OXRUQldIYXNWaG1IeVluVXJh
aHowNUMvRlFHZ2J0TE50K2pMOTJBMWsKLS0tIEFwdVo5djJURU16aUdMeEhFeUsy
QTA5bjZFWTIyeG00ZDVTbVY0UWN3WGMKReL4f5v41eEIogPSqMuiSVml1stAAAf3
nedjWc5s2C5mO3IB+iU7uOWF6P5kIrXU4Tvmwju2E8yw4v2lmsfZLg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyTjRhME5rSXU4bGlzVXBu
ZS9WanpqRU1zUVJxM01Ld0NiYVR1ak5OZ2dJClRSSTJNSTdoTEQ2UzRVSlhRbDRx
NWxId2tsbUhFd0lOUUY4dTJTOG5tMW8KLS0tIDRpcThPaTkyQ005aXhqSnVTYkN2
LzhKU1FUeklTd1RuUk1lSVYwK3VLTEEKcKHaluWQ+Wgs9qI0qvyfnx+goSymL9wc
bJ0lxptRr0PGHdKhBRRlSe6HCMfshIoTktooUT6vNv4AsPmZuJZhJg==
-----END AGE ENCRYPTED FILE-----
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDd2JvVHJUNXc0eGc1TTJi
OGhjSDFkaC93dG9EWDF0WThoQWV2SjE5S25FCkdBRU55MElTdHZnUmU1ZGF5b1gy
aFdyZGJyUzFpQVFRTVBReXp6MXZWbncKLS0tIHZaYmRLeld3UHdwWjc0WGNBQ1k4
VkMyN1FNNysxc2RMTzlOSGlzd1RSazgKXBumJC7hLOJ3rcG2x80L/mEPGMbWKGbG
En66KslOsgX/LugQmRey82ezDhqhnvpHe+sLWRaf9JfM+zCRg4mUMQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bEFSaS9TWmJ6MkRWd09J
TVdGTGN4SS9kVE5yMlRTUHBWK09pYVZpUTFVClJmZ1pVSm1XZFIxeXkrb2gxbTYy
eDVvT2RUNHBOWDRSa1Y1MkpxMGhzbDAKLS0tIEl2ZE1Bb0U4NGZ3QVg2ZUpRQ0o5
YUFNOS8xUnlKOXZSZ0M2ai8yNmNxTGsKHhwRXY18pGLitckX5vxFRJyqVL4VgWbw
+Gy+IwB7fJXoYlKHJXFfLOfhifCvrgouTcqV0ckPx/WYWSUKNDO89A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
- recipient: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyZW85bDNRUzg2QnZKczly
VXJiRlFLVVJDSk91bjV5ZU5HMzVjWCtnSHpVCllheVh0WE82NlQvTXNwak5mK05n
aFlDNXM5Smw4dHFtSHRnSitUN1hhYWcKLS0tIGQ2akhocXArbCs2ZlhCU1RjUEE2
aFZoRE5DRC96bTVqWkZ1VmV6TjJjZzAKXfP/7E4bjSoPRENvk0gThEaNuJUgukwR
jpa5By90xamqzIRXSmnrNX20owfWugzzuAUjdE9/kiSz5R6Csi3LuQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwQnNWTC9TUHZZZ3JoUnBL
Vmh4OEwvNzVzUS9HVVpaT3lBWlllbFZsSzJnCmhCR0NDT0Q5ekFCSDdoclB4Sml3
ZjVnV1BpTkhmS0FUSDZmWGk4WGRrVGMKLS0tIGFCTUk1dzBaV1VBR0pLUGJtWDJh
RVd4K2Q0b2Vqc2F6b1hmQng1RHRheWcKOHUOZm+GjvHOKI3VRlgPeH3IKojGB9F4
YhkW83lF1Wl0XYnHEUya25bMSYLzQHOPiy2I7n4K45uk8hKQmrKE5w==
-----END AGE ENCRYPTED FILE-----
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2L1JKWlozOUpvTjBkNjRX
V3hBYzdLT3k0dVBGSFJLY2crQUxEeUd1SEN3CkEzM3ZRQm93SnFiTmlianM2VUdL
SVdpTm1DNHVRUlU0Mkd4eUxlMzFrSTQKLS0tIGZ1STFYQjlSc2dpNWVBK0Z0Z2g1
SlhoUEtZcE5PbTJCM2haME1vR25QelUKmdhCrRs1RzWIx/1Zjmas50oFkGjjhlvD
m5gLBMs6VSe871DczImP/l5ViqCg9w83ZYZI0c2Usn+9i016HOnFBg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQbTAxQnJzc2xVZUh0VFBh
RUZiN3l6eHNLYzUyVVdqUVl5WjFBRW9uamhVCkRJM1hGWndXclNoVlMvcjQvZTlm
K3RxL2xrb0txZk1XaGlHTGRZbVFQemMKLS0tIFZaRkFQWURzcnRaV1lqTGhMTFp0
TTVNU2NnLzhlR0dTLzBkdThpeURWL28KHxERu5qGbXlZnTw9bHHe7AgCOZ3PI99R
91bVqvche0QPiESnu0Od4sIHID5g5F5+EBw53lQgjEx0c4Q1GFQfFQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQbWVvaWpQZE1CdXFjMVVi
SFFwSmRxTFdxUGtpQWZNNXlWWDQ1cXkzWTFNCm5WRlN0dVlFVW9ONXJQb2lic0oz
dXhMSk1RN25qT2VXZGkyVmY3TTJvT1UKLS0tIHBjK293bnRhLzRCOU9hNXQ4MVNN
K0ErNEhLNWFoc0hXdTE3MnBqT2pLblkKx9ww+qLJKdikom59GGth8/lWWmzKS2k+
d+4votCaQYJtQbBuHUcKAKUeKFl0jBMJPoRO4XodrprXHtpU1l+nUg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnQXlGVEJDTDlvTDI1RXAy
ZkZSN2NzVHNmb3U3N2xRQ3EyWmtaYmZyMEM0Cjh0QWh2ZUZycFVjbEJmdDNEK0pZ
MGFSTW13TkRmU1RkZzVvVTZpQkMvaW8KLS0tIE5JRUtOSHErSmtnN1krZFVEd2hs
OXNMaEc4ZUw0RW9qaHhiUVdIZVZrVlEKMBG7NyFXqT6zxwxIq30Nj+uWz/zhjbhU
y4JqomFHxzwySEQD/1rfnTIJpmgpJNbyvRo4ToLDsM3B8TWk6D7/MQ==
-----END AGE ENCRYPTED FILE-----
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyOUtTRU5RaEVUaVQvM3Ro
L3FhR3NhK1lHNFI1TjlBUGJCOXowR3F0VnprCnBNL1ZKbHJkcEZhbWpTQzFIUnBX
NmxjTDNCRmVhZnNOM1pwRGdTZTBYZk0KLS0tIDVIcUF4MHNlVXBKVnBGSk1vd3JD
OXBHekx1RlpSYlFnYld3T2Nza0R5bmsKt4mBjr+YP/li9Wq6GL5eJBGrSBi2GcE7
GjP1pYyt0nsazuRrueKXWE12p4JWz0CI7vUsLfrxd9JiEdrPuC9hrA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArMnFJMXR3SFBuTGt2TWdL
bUZMQkU3dTlYS0xqbW9xN3JrY28rWEtIcHpFCmUxbFBQYWgyM1dPOUtLVmEwWDJj
N1RtUkVVSHFmUWkyZFBndmhGeFgwc1kKLS0tIHNMSjVYemQyTUlqVGVtVlBHU2cx
eEh1MmhQRFNyNE1NSDdwWk5BRCtDMFUKZByCL2Wj0X+lwUo06PHwOiaJhzqOMVVt
Rj/pvynxLV4d0RBzwpgdL9uV8VzTED4GW9wotODbhEUtdlpSS1YOGg==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-04-05T09:43:00Z"
mac: ENC[AES256_GCM,data:B3G5BlUA1Rq1WxOnrPtm+Ag+TMBxgTAGCvGd3YY6GE8gvBZh0u2NqWcI3/dEaY/2hdv8LO011nP6oOHAEU10FzsMTijmaHOVZers31Ov+zr1/X1zOAKA4c5LtgRhVOJ2ugKTwuTeuTcouJj1Gz94YT6Dc4kebnOfOB4RY1poyvc=,iv:raTWQ/u46vNoW3ZlXwct6DChq5/rk9TxqYVQL4hDyug=,tag:fuVgIVSSfJegTNMHAiK4Rg==,type:str]
lastmodified: "2024-03-26T22:31:10Z"
mac: ENC[AES256_GCM,data:OujhtajsuAQrC5KNFPxqjMlHAS9tpjvvsu8LRZf8XKrMui3ZBAHO2TdF7z/sAEB9OPlPJZGZU4jMDNXZkIi1zv3mUDNJXPs0oitgIEXSYCDHcZQea093hSMd4tX1yLQM3M5GH1aFZDXfIpKT7UvLjiVv+8aXp7BLQNNilbFKV1g=,iv:1OD4SuMUSD3fcuO4QiZpjij49JHwKqDJvXOT0wyJ0zs=,tag:VW4KiInBhIpbaaAS18eNUQ==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1
+36 -36
View File
@@ -1,7 +1,7 @@
djangoSecretKey: ENC[AES256_GCM,data:dVq/508Au7M/Z0KqVKfaAQ1Qv0NR9EixneJXgcQLYPqr1zALAs8YdTfAHO97ObkYguM=,iv:TDVByohsak3njekbj7gPcYqWzBAxFAEn8Y7EpnyZiRM=,tag:Qfsp/PTbJghPNsJJVf5mnQ==,type:str]
djangoSecretKey: ENC[AES256_GCM,data:trxlec0na+q/9kh7kkZpuyp1Zjx9uXyL3fS+UEsGsN612X5tVLtAUuSx8aGYr/f6uYs=,iv:+gjpSUBq9TEmeLYC18vT2HOi869W9xgzxC5QchI1iUg=,tag:zvKVaqwY43bRWOhhr3dFcg==,type:str]
oidc:
clientId: ENC[AES256_GCM,data:nTlAk7Vr/FmofOBVAzI9cj7PXFHatGyVsM0ujGP9uxiP9Cdt,iv:bPQ8W2jvZ+k+dDTJngCa1iVkWUj5RJhgx+Hm4uNt7Uo=,tag:PyjfXpXvQFw6886GGzS7qQ==,type:str]
clientSecret: ENC[AES256_GCM,data:hSPwOFDXP+ZPDA+kLYhdYTUhHC19qad6oTEuM4tvwN/+ZEmI8TCMadQoMGUdAHHQGogk3fdnnQyNW7CdLwz0Xw==,iv:z30xOFiObn4vPanJrKjeHtpDzUMI9XnivgokoC5zDL4=,tag:+50pbXgqmMZHCWMnnoi7ZQ==,type:str]
clientId: ENC[AES256_GCM,data:3A6nchWO8pVLIlWLRL3TBXCuwoo4dyrvtrfqrBqStLJRUl2A,iv:WZwTDGphAJ2KRN6cpj4HpZM5AsLywsjdI9m9tuhjigg=,tag:7GDMJhF0jrZghPENdQF9xw==,type:str]
clientSecret: ENC[AES256_GCM,data:X2pWXOrxlt+Sbf6Wq7g4Rz65AOXsAB/U35sJDFXHfZpT556xKekDmW/isD1R3kP8OTtigVi0gSrOvMePC9tgmg==,iv:sTD3nXIx2Z52pzO8A8VNpcQJ9Or9KMAxTG5/fYL0oTI=,tag:CCKemFbgJNDCY2bwNpqJiA==,type:str]
sops:
kms: []
gcp_kms: []
@@ -11,59 +11,59 @@ sops:
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRYy90Z3ZxbjdldnRoaDJ2
VHlHeDNQVkY5ejZ0Y0F0NGJ2cE9uNlRkVHdBCkQ4ejdSZmxEWmpodDRvcGFTa2ND
VlpXL2lGUVJncHZURSttbEw4cC9WekkKLS0tIHhrWFpCRDJvNkNOYWZzYnVGb2l2
M3NoaGpVSlF0N1k2UXNVNFRTWTlNa0EKaGkcGVgeJFTv844UQ6tBY5hT18PoRhh4
uIL6bH2Bs6P+wIbmuqwKhba8muS9rWbvFJppD8N/htJT2ZzXgmZAvQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1SzRHcERkUUdLVk9XOG9M
MDdjSGZDam9wbUNwMFZaZ0I0bXJZSDRhTUJVClh3YkRXR3lSRGNrditwejZaeWVD
MVIxNG0yMjNzNlVNVFc2N0Y0dXlaSkkKLS0tIHpWcGZNRkoxZDJJcDg0Y1hJOWM4
YTdVVC8xU0p1RTZMTmFSQ20rdGFydGsKb/iZA5lO/QdPnNIuC3irxT2Ajh4C5SES
p74VU20kUNFt7WsHMUBlkxbC2p4Mw+qacjIGqpezC+69UlSwTXawMA==
-----END AGE ENCRYPTED FILE-----
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxUkhaS0lEbGRrRkxpa2wz
VUlRMFFYbVJpa0tJSjBKUGlPVXE4NW5XUmdrCjV0SUVTNUJCTXRnbEpIMm44N05L
R1pWTWVZZzZHQ0U2ZGVQdk9kMmpZUncKLS0tIFJhZ2V1aCtYTHJWNFZ3bWpibTBs
QWJ0ajN1U3NjVHVjTE9HWnRVOWdyWEUK+Fu4p4oAwAH5nhaWKo6C/MhdAo7IbkAt
qarRcXRIRlr29K4IpmbbiIZZA/e1uWxMxD1Bafj4pIFppKTQFeIkSQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzVjZTdENzQllqayt4L1RE
aWFTaHQ0MXpCWFIxN1ZySmFIRXlzSEZrM0c0CmpSUXFISGF1S3IyMjNIdEdSTEJs
MW0rQmdRRWxVREU0c1dUWnpNQW9kbHMKLS0tIE9SVUFERk9CT2RDSmdjYjlzUnNm
MTJGQTRZTzQzeEVrVnFxZVErdWpKMVkKMZzombPphRq0mEKxQotJfLBdBQz+PDJU
YbiTe9jLLWeNDdoqMKNmcAtW0tBL0r3KWtGIZRWDV+IXXXbkVRubnA==
-----END AGE ENCRYPTED FILE-----
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
- recipient: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWmFBWWhtRlR4emtWRXhG
M2Z2MVZVWFpETU9BTlRZSXljMzNsa3E1dlVnCjdWeXhLYXNPdCs3R2FTK2tiK0VD
WGc5cWYyUEtvMmVJbTRPZ25zdDNzd2cKLS0tIFNJWnd5c2tQZkwrdGx6UE1jOHpO
L0hlY0NLdS9FVk5FdW1md2lmU0lpQmMKZ4vZhT4Fmii9HHhJ+W9/BUkmzmzXnMHg
q8jk+pDfNR9P8Lw+95Q8DjV6uvLpw9XjOkQzm6UCNKk9/M17c4EHeQ==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzSnJrUTFiZm0vRldkS2hr
cUl6bmpHbDBtTmJRWnp1YTR1SzZFMmZmc3dnClVra25QNnBURkZySGRuZlFHNWk2
dUhvNVhqaWU1bVZVQlpyeDh4eXo3M0EKLS0tIFJHbXU3eG5velpZSTdESysvcVFr
MzBjM0plTnZGakhqckJ4L0NVVGVBSEkK3FI3omG4PXTmBxnnUVAwyRA2B99rzbtx
GqqSqCFYfn4aSFz6kz4+hxzv9rEgMBWhqpA6dpfBbz3SxmbDTW8V6Q==
-----END AGE ENCRYPTED FILE-----
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbENsZjZsTlp2ZXo5em5O
U0ZaZWtkZjFFK0I0L3pFZDM2c3dUS09abDJrClV3ejFjU1NwZzZZaEhqNFUvQVNL
K08xMm1pR3dTOHZyY0dYSlo0TG9iRm8KLS0tIGc2ZVBzRzV1WW03VUQ3ZU4wVGZn
YnNmL1pyQk4ySVMxbXh5V1pGdDlaTHcK4R15lD5ryKO7CvgpOGmfSu8i7lbkT9EI
lWC+AXSfKmhAZzXihrgmANcoIk4zitjHOoJN/PK9DAZSskhBqbm8qA==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5SEZlcnNDRlNSTWRrdG1S
KzRMZ2RBdzVEQVBwTFNXRVk2dG5DUnc0MjJnCnVoeVZ2WnRVaWFMMjBESTZUeHQ0
RllWdnNXRTRkbWZXbldVVHc0QldhQkkKLS0tIDhuSU1sNXU5R2p5LzI1YXp2VEVo
M3JqeGd2MnQ0QnNNM0cyWHpUYmlHc1EKnZazjekMiytOi1jLktn9DoaRHT0lQP2s
GYvHZ4+xM3LwobmnVJCq1bXnl8fBuVKZbTOG+WeJbxNJq9fSk2I6rw==
-----END AGE ENCRYPTED FILE-----
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMEJTOG5Jb2pjR3NaQVhV
WkQ1VnY2Ny9UcDlpcTZnc3FFQkRCRlJvY0I4CmNEMjE1Rm9KZ3BzaHhMUWpFczlK
K1JtNlZMcno4cEROMHpYd0R1MC95QzQKLS0tIHBBdWRGRVFyME1tU1hrUk9Ha2pH
eEx6Z2VHSHZOTFZhdWtVVUJWTGpObDgK8MB5SYG4oJswJEqWa274FK6YXlMoFO0k
cGibj3uCo4XWaHdV3ik9GrKg68yo3yrgsc7pyB8aSHfgs47teO6Qhg==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRSDRQRlc2dUVPWjN4Tlpw
cGpXeGtxc1JWOVdSc0s1WGRrTUl0UmcyVmhrCmNqWkZuZDJYLzM5YlhuVFc0bEln
aExWZ0F6bnBzYkd4V0xKeFZudHplL1UKLS0tIHUvSUtadlRCeC8yNUVEVjZEVm1L
RXVINFI5bHdDWGNjUjNsRU8xbTd0T0UKYL3phOso3YNi6tTWbpHdXW/Pae6uzz17
AmLjdjD4KUVTlu6bhzSrazP+3EDtO5X3S57nladHcvxQPdqgAJQ99A==
-----END AGE ENCRYPTED FILE-----
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXakoxT0JQbEdoMUN3L216
MmRFdFRsTDhSd0tKaUpNTTlFMUptMUtOWDI4CmFnb2hTeXIzUEluTllpbStxcVZI
RytwdmZqeUhKVUQrK3BhUTRybEo3cDAKLS0tIDJXUWN3S0F6SXB1dU4za1IrZmYz
WnJhTHJvZmVuT2NkZDJnMGxBMS83S1EKY6Up5cDbV4vVZLzxm6Z7r+pTRH9Gfoun
Li7lS9Vv9WVs7yLFbJ2Iu0qEIkgkJetzMhV/bo305nai3bcZfvm1bw==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtV3JvKzZnaGN4WTBsTnc2
TDNNYk10ZWhOWHAyZjlWWGRsMDJuSHI3cXg0CllVL1VIVitSLzNieEc2ZWppdVFi
RmlZMjE2cWdEcVhBenl2UG5McUZUK0UKLS0tIE1XMlN0YzZWVVVOdlJsZVZ1VTVC
bnBRVTJYUzYzNEM1eU8vQzlqdk9lY3MKM9g8opHNjlm2cAkVzc9LXt2TM+Jmq8Of
DbVFbegKV8lgnLKdmWVeKDtLFHiZj4dQclvwxbNuIk2QvEj9Wam7uQ==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2024-04-05T09:42:43Z"
mac: ENC[AES256_GCM,data:RHUdOrgnbTCzrcyoWKfz7qC3i81ZUIyxBzBl3xQH/kCXsVbIPhtRUvFLwgd9uhNNiiBjPfx68GwiXatSko8vPf0rj2FVaC+w6yf9RTItxWqGETS18Waf5etsFCMhJ4LYce79DJ8KFtqjB64VYF3BVgX9Cif7wy1jGklbN7cGgjg=,iv:sllxfa74NAQTGHuBufOS6jH7VSOu5JsvwzNfBK5QRKw=,tag:WN6vWKtiPkZdbaJ04Q/VRA==,type:str]
lastmodified: "2024-03-26T22:31:45Z"
mac: ENC[AES256_GCM,data:jC6rd1YvDrDZxY4gfPGYY6rwygTgeL0w95a81aGAElthOl+r9eaNsqDvkOYaEuoZctdIP4MmaGfC8ZjBGrK+WbTacxHNmv9OaB43STrEqBmZWVDRUJZEgYhdQGMOW0jpDc08WaV67J1ViEuH4bcaUCwHrT//HQUf3e+25ZsO4R8=,iv:tf6xbLvxzURT0FcV0ZZYaT0b6v6GnIO854NLZZvCS8Q=,tag:5lz71fju3WHwQKTyW+lkAw==,type:str]
pgp: []
unencrypted_suffix: _unencrypted
version: 3.8.1
+1 -1
View File
@@ -20,6 +20,6 @@
},
"homepage": "https://github.com/numerique-gouv/people#readme",
"devDependencies": {
"openapi-typescript-codegen": "0.29.0"
"openapi-typescript-codegen": "0.28.0"
}
}
+4 -4
View File
@@ -88,10 +88,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
openapi-typescript-codegen@0.29.0:
version "0.29.0"
resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.29.0.tgz#e98a1daa223ccdeb1cc51b2e2dc11bafae6fe746"
integrity sha512-/wC42PkD0LGjDTEULa/XiWQbv4E9NwLjwLjsaJ/62yOsoYhwvmBR31kPttn1DzQ2OlGe5stACcF/EIkZk43M6w==
openapi-typescript-codegen@0.28.0:
version "0.28.0"
resolved "https://registry.yarnpkg.com/openapi-typescript-codegen/-/openapi-typescript-codegen-0.28.0.tgz#2becaba1c655feb851d04e1f2fac07c55d168e57"
integrity sha512-BZTsMUwhA/h2zCzisjagLUPQNHE64N1EN074yGB+WqA0LFlJwy8sKQYrXH5G4phbjj9KSPx7xuWKO4hkPIOARw==
dependencies:
"@apidevtools/json-schema-ref-parser" "^11.5.4"
camelcase "^6.3.0"