Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e502368c8b | |||
| e4da9b23cd | |||
| cc157904d0 | |||
| b06e30f436 | |||
| d124b57d16 | |||
| bc182fe313 | |||
| be364d9227 | |||
| 681065dd85 | |||
| a1065e55e0 | |||
| 6b15954d05 | |||
| 23cf6c31f7 | |||
| 5b2988491a | |||
| 669633285c | |||
| a1f9cf0854 | |||
| 2f1805b721 | |||
| 711abcb49f | |||
| e68370bfcd | |||
| ae1acd8840 | |||
| 54386fcdd3 | |||
| 45a3e7936d | |||
| ebf58f42c9 | |||
| 7ea6342a01 | |||
| 6d807113bc | |||
| 5455c589ef | |||
| 9ec7eddaed | |||
| 7db2faa072 | |||
| 6e0b329b09 | |||
| c7aae51d25 | |||
| db40efb360 | |||
| 3ddc519d9e | |||
| e20960e3e1 |
@@ -16,7 +16,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' # Makes sense only for pull requests
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: show
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
github.event_name == 'pull_request'
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
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@v2
|
||||
uses: actions/checkout@v4
|
||||
- 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@v3
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
@@ -214,7 +214,7 @@ jobs:
|
||||
working-directory: src/mail
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- 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@v2
|
||||
uses: actions/checkout@v4
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- name: Install development dependencies
|
||||
@@ -284,7 +284,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
- 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@v3
|
||||
uses: actions/setup-python@v5
|
||||
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@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- 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@v3
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
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_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_age__list_0__map_recipient=age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGRS9pcEprcHlJSHJaWWlQ\nZ0ZTbzhoUXYxUXlNdDVyQXdvQUhVcjFya0NvCk1UOFNSUXRsMTl3Zy9ibmhMWnlN\nSUVCVW9NaDAwYk0wdlRvREZDSHI5cU0KLS0tIDhhRnNoS0xoNjE0d3FKaXZhN1V2\nbW5KOHFGcGpiOUxYWUVxZWFCejJ1SW8KNY6H8A3DNhJ2tqy13md0icS6fzHmd8cH\nah6FyrgxXa6zJbmC/bKRVPiAU7D2xAgkyqS4nzvXjeAaEMef+a97GQ==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_1__map_recipient=age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
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_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_3__map_recipient=age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMEdlbi81UXVCZ1o2RjJu\nbUNkdXIxcTJoU004MUxtZWE5dWtHUm01ZmpVCm9RRzNkVWhsakFRTGRBbEdMQzhw\ncU54ekphSENPNlFsOXp1blQ3SmlaelkKLS0tIE9pTW5uQ2JrbUFQcGMwL1lqL000\nekdNa2pQWHA1cXBaTGxZT1htNjhzcmcKGD1xp6Wc2CYzmkI2blmX4xt8It5HPX/w\nj5oynxnDwwPkknRdZ1bDlre5fXTzKGd917RcU2WUs9q9cbZHTmTQQQ==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_4__map_recipient=age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
||||
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_enc=-----BEGIN AGE ENCRYPTED FILE-----\nYWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBhVEVMcU9nYUhPMTU4d2Jj\nclR1bUR0QlZEdFcraDZ6ODhwcTJsZ1BCNUR3Ci9MamhHYXN2dU9nRjNkbnUzQ1U3\nMjRjVG5kU3JFSmJmQ29UV20wSmVHbk0KLS0tIGp0aVBlNzI1V3lGYy9ONzhlVUR0\neTlPN2drTTAwOFlCUjA2U0FCY1lFVHMKzolngi5XjFQKUnwLpdpqmBDPuY0Bsurk\nyvKE/Lou9Pcm7OhZePTwoVIcBtS313vzh8xmnVeuJPpIzLjkfvJwRg==\n-----END AGE ENCRYPTED FILE-----\n
|
||||
sops_age__list_5__map_recipient=age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
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_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_unencrypted_suffix=_unencrypted
|
||||
sops_version=3.8.1
|
||||
|
||||
+2
-2
@@ -2,14 +2,14 @@ creation_rules:
|
||||
# Here we have
|
||||
# - Jacques key-id: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
# - github-repo key-id: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
# - Anthony Le-Courric key-id: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
|
||||
# - Anthony Le-Courric key-id: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
# - Antoine Lebaud key-id: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
# - Marie Pupo Jeammet key-id: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
||||
# - argocd key-id: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
- age:
|
||||
age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x,
|
||||
age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7,
|
||||
age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv,
|
||||
age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg,
|
||||
age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3,
|
||||
age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa,
|
||||
age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
|
||||
@@ -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 --email admin@example.com --password admin
|
||||
@$(MANAGE) createsuperuser --admin_email admin@example.com --password admin
|
||||
.PHONY: superuser
|
||||
|
||||
back-i18n-compile: ## compile the gettext files
|
||||
|
||||
@@ -40,6 +40,10 @@ services:
|
||||
- postgresql
|
||||
- mailcatcher
|
||||
- redis
|
||||
# FIXME: temporary configuration to connect AC and desk
|
||||
networks:
|
||||
- default
|
||||
- dataprovider_outside
|
||||
|
||||
celery-dev:
|
||||
user: ${DOCKER_USER:-1000}
|
||||
@@ -169,3 +173,10 @@ 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,3 +33,36 @@ 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-----
|
||||
"
|
||||
|
||||
@@ -53,6 +53,15 @@ 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"""
|
||||
@@ -67,7 +76,7 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
)
|
||||
},
|
||||
),
|
||||
(_("Personal info"), {"fields": ("email", "language", "timezone")}),
|
||||
(_("Personal info"), {"fields": ("admin_email", "language", "timezone")}),
|
||||
(
|
||||
_("Permissions"),
|
||||
{
|
||||
@@ -88,13 +97,13 @@ class UserAdmin(auth_admin.UserAdmin):
|
||||
None,
|
||||
{
|
||||
"classes": ("wide",),
|
||||
"fields": ("email", "password1", "password2"),
|
||||
"fields": ("admin_email", "password1", "password2"),
|
||||
},
|
||||
),
|
||||
)
|
||||
inlines = (IdentityInline, TeamAccessInline)
|
||||
list_display = (
|
||||
"email",
|
||||
"admin_email",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"is_active",
|
||||
@@ -105,14 +114,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", "email", "identities__sub", "identities__email")
|
||||
search_fields = ("id", "admin_email", "identities__sub", "identities__email")
|
||||
|
||||
|
||||
@admin.register(models.Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
"""Team admin interface declaration."""
|
||||
|
||||
inlines = (TeamAccessInline,)
|
||||
inlines = (TeamAccessInline, TeamWebhookInline)
|
||||
list_display = (
|
||||
"name",
|
||||
"slug",
|
||||
|
||||
@@ -52,42 +52,22 @@ class UserSerializer(DynamicFieldsModelSerializer):
|
||||
"""Serialize users."""
|
||||
|
||||
timezone = TimeZoneSerializerField(use_pytz=False, required=True)
|
||||
name = serializers.SerializerMethodField(read_only=True)
|
||||
email = serializers.SerializerMethodField(read_only=True)
|
||||
email = serializers.ReadOnlyField()
|
||||
name = serializers.ReadOnlyField()
|
||||
|
||||
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."""
|
||||
|
||||
@@ -202,7 +202,7 @@ class UserViewSet(
|
||||
Prefetch(
|
||||
"identities",
|
||||
queryset=models.Identity.objects.filter(is_main=True),
|
||||
to_attr="main_identity",
|
||||
to_attr="_identities_main",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -245,9 +245,6 @@ 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
|
||||
)
|
||||
@@ -378,7 +375,7 @@ class TeamAccessViewSet(
|
||||
Prefetch(
|
||||
"user__identities",
|
||||
queryset=models.Identity.objects.filter(is_main=True),
|
||||
to_attr="main_identity",
|
||||
to_attr="_identities_main",
|
||||
)
|
||||
)
|
||||
# Abilities are computed based on logged-in user's role and
|
||||
|
||||
@@ -3,6 +3,7 @@ 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
|
||||
@@ -14,3 +15,11 @@ 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")
|
||||
|
||||
@@ -120,13 +120,13 @@ class ContactFactory(BaseContactFactory):
|
||||
|
||||
|
||||
class UserFactory(factory.django.DjangoModelFactory):
|
||||
"""A factory to random users for testing purposes."""
|
||||
"""A factory to create random users for testing purposes."""
|
||||
|
||||
class Meta:
|
||||
model = models.User
|
||||
django_get_or_create = ("email",)
|
||||
django_get_or_create = ("admin_email",)
|
||||
|
||||
email = factory.Faker("email")
|
||||
admin_email = factory.Faker("email")
|
||||
language = factory.fuzzy.FuzzyChoice([lang[0] for lang in settings.LANGUAGES])
|
||||
password = make_password("password")
|
||||
|
||||
@@ -177,6 +177,16 @@ 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"""
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.0.2 on 2024-03-05 17:09
|
||||
# Generated by Django 5.0.3 on 2024-03-25 22:58
|
||||
|
||||
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')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='email address')),
|
||||
('admin_email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='admin 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,6 +144,23 @@ 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.'),
|
||||
|
||||
@@ -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
|
||||
from django.db import models, transaction
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import lazy
|
||||
@@ -24,6 +24,9 @@ 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__))
|
||||
@@ -74,7 +77,7 @@ class BaseModel(models.Model):
|
||||
def save(self, *args, **kwargs):
|
||||
"""Call `full_clean` before saving."""
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class Contact(BaseModel):
|
||||
@@ -157,7 +160,9 @@ class Contact(BaseModel):
|
||||
class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
"""User model to work with OIDC only authentication."""
|
||||
|
||||
email = models.EmailField(_("email address"), unique=True, null=True, blank=True)
|
||||
admin_email = models.EmailField(
|
||||
_("admin email address"), unique=True, null=True, blank=True
|
||||
)
|
||||
profile_contact = models.OneToOneField(
|
||||
Contact,
|
||||
on_delete=models.SET_NULL,
|
||||
@@ -199,7 +204,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
|
||||
objects = auth_models.UserManager()
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
USERNAME_FIELD = "admin_email"
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
class Meta:
|
||||
@@ -211,9 +216,32 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
return (
|
||||
str(self.profile_contact)
|
||||
if self.profile_contact
|
||||
else self.email or str(self.id)
|
||||
else self.admin_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()
|
||||
@@ -225,8 +253,10 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
||||
|
||||
def email_user(self, subject, message, from_email=None, **kwargs):
|
||||
"""Email this user."""
|
||||
main_identity = self.identities.get(is_main=True)
|
||||
mail.send_mail(subject, message, from_email, [main_identity.email], **kwargs)
|
||||
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)
|
||||
|
||||
@classmethod
|
||||
def get_email_field_name(cls):
|
||||
@@ -368,7 +398,7 @@ class Team(BaseModel):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
"""Overriding save function to compute the slug."""
|
||||
"""Override save function to compute the slug."""
|
||||
self.slug = self.get_slug()
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@@ -435,6 +465,34 @@ 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
|
||||
@@ -485,6 +543,34 @@ 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."""
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""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)
|
||||
@@ -0,0 +1,219 @@
|
||||
"""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
|
||||
@@ -0,0 +1,11 @@
|
||||
"""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()),
|
||||
]
|
||||
@@ -0,0 +1,61 @@
|
||||
"""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
|
||||
@@ -0,0 +1,54 @@
|
||||
"""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,9 +2,12 @@
|
||||
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
|
||||
@@ -174,3 +177,60 @@ 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,9 +2,12 @@
|
||||
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
|
||||
@@ -148,18 +151,77 @@ def test_api_team_accesses_delete_owners_last_owner():
|
||||
"""
|
||||
It should not be possible to delete the last owner access from a team
|
||||
"""
|
||||
identity = factories.IdentityFactory()
|
||||
user = identity.user
|
||||
user = factories.UserFactory()
|
||||
|
||||
team = factories.TeamFactory()
|
||||
access = factories.TeamAccessFactory(team=team, user=user, role="owner")
|
||||
|
||||
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,11 +266,15 @@ 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) == results
|
||||
assert sorted(results, key=normalize) == results
|
||||
|
||||
response = client.get(
|
||||
f"/api/v1.0/teams/{team.id!s}/accesses/?ordering=-{ordering_fields}",
|
||||
@@ -282,4 +286,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) == results
|
||||
assert sorted(results, reverse=True, key=normalize) == results
|
||||
|
||||
@@ -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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="john doe")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="john doe")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="john doe")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
user = factories.UserFactory(admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="john doe")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="eva karl")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.email, name="john doe")
|
||||
user = factories.UserFactory(admin_email="tester@ministry.fr")
|
||||
factories.IdentityFactory(user=user, email=user.admin_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(email=f"user-{i}@people.com")
|
||||
factories.UserFactory.create(admin_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(email=f"user-{i}@people.com")
|
||||
factories.UserFactory.create(admin_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.email
|
||||
user_email = identity.user.admin_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.email == user_email
|
||||
assert user.admin_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.email is None
|
||||
assert user.admin_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.email is None
|
||||
assert user.admin_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
|
||||
email = mail.outbox[0]
|
||||
|
||||
assert email.to == [invitation.email]
|
||||
assert email.subject == "Invitation to join Desk!"
|
||||
|
||||
@@ -2,12 +2,16 @@
|
||||
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
|
||||
from core import factories, models
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
@@ -39,6 +43,94 @@ 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
|
||||
|
||||
|
||||
|
||||
@@ -31,20 +31,20 @@ def test_models_users_id_unique():
|
||||
|
||||
|
||||
def test_models_users_email_unique():
|
||||
"""The "email" field should be unique except for the null value."""
|
||||
"""The "admin_email" field should be unique except for the null value."""
|
||||
user = factories.UserFactory()
|
||||
with pytest.raises(
|
||||
ValidationError, match="User with this Email address already exists."
|
||||
ValidationError, match="User with this Admin email address already exists."
|
||||
):
|
||||
models.User.objects.create(email=user.email)
|
||||
models.User.objects.create(admin_email=user.admin_email, password="password")
|
||||
|
||||
|
||||
def test_models_users_email_several_null():
|
||||
"""Several users with a null value for the "email" field can co-exist."""
|
||||
factories.UserFactory(email=None)
|
||||
models.User.objects.create(email=None, password="foo.")
|
||||
factories.UserFactory(admin_email=None)
|
||||
models.User.objects.create(admin_email=None, password="foo.")
|
||||
|
||||
assert models.User.objects.filter(email__isnull=True).count() == 2
|
||||
assert models.User.objects.filter(admin_email__isnull=True).count() == 2
|
||||
|
||||
|
||||
def test_models_users_profile_not_owned():
|
||||
@@ -91,11 +91,26 @@ def test_models_users_send_mail_main_existing():
|
||||
)
|
||||
|
||||
|
||||
def test_models_users_send_mail_main_missing():
|
||||
"""The 'email_user' method should fail if the user has no email address."""
|
||||
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 pytest.raises(models.Identity.DoesNotExist) as excinfo:
|
||||
with mock.patch("django.core.mail.send_mail") as mock_send:
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
assert str(excinfo.value) == "Identity matching query does not exist."
|
||||
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)
|
||||
|
||||
with pytest.raises(ValueError) as excinfo:
|
||||
user.email_user("my subject", "my message")
|
||||
|
||||
assert str(excinfo.value) == "You must first set an email for the user."
|
||||
|
||||
@@ -0,0 +1,341 @@
|
||||
"""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"
|
||||
@@ -0,0 +1,70 @@
|
||||
"""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,
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
"""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(
|
||||
email=f"user{i:d}@example.com",
|
||||
admin_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", "email"))
|
||||
users_values = list(models.User.objects.values("id", "admin_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["email"]
|
||||
user_email = user_dict["admin_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(
|
||||
"--email",
|
||||
"--admin_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("email")
|
||||
email = options.get("admin_email")
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
user = User.objects.get(admin_email=email)
|
||||
except User.DoesNotExist:
|
||||
user = User(email=email)
|
||||
user = User(admin_email=email)
|
||||
message = "Superuser created successfully."
|
||||
else:
|
||||
if user.is_superuser and user.is_staff:
|
||||
|
||||
@@ -31,3 +31,14 @@ 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
|
||||
|
||||
@@ -7,6 +7,7 @@ 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()
|
||||
@@ -35,6 +36,7 @@ urlpatterns = [
|
||||
[
|
||||
*router.urls,
|
||||
*oidc_urls,
|
||||
*resource_server_urls,
|
||||
re_path(
|
||||
r"^teams/(?P<team_id>[0-9a-z-]*)/",
|
||||
include(team_related_router.urls),
|
||||
|
||||
@@ -301,34 +301,9 @@ 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
|
||||
)
|
||||
@@ -347,13 +322,81 @@ 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):
|
||||
|
||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"boto3==1.34.75",
|
||||
"boto3==1.34.84",
|
||||
"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.3",
|
||||
"django==5.0.4",
|
||||
"djangorestframework==3.15.1",
|
||||
"drf_spectacular==0.27.2",
|
||||
"dockerflow==2024.3.0",
|
||||
"dockerflow==2024.4.1",
|
||||
"easy_thumbnails==2.8.5",
|
||||
"factory_boy==3.3.0",
|
||||
"gunicorn==21.2.0",
|
||||
@@ -47,8 +47,9 @@ 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.44.0",
|
||||
"sentry-sdk==1.45.0",
|
||||
"url-normalize==1.4.3",
|
||||
"whitenoise==6.6.0",
|
||||
"mozilla-django-oidc==4.0.1",
|
||||
@@ -66,7 +67,7 @@ dev = [
|
||||
"drf-spectacular-sidecar==2024.4.1",
|
||||
"ipdb==0.13.13",
|
||||
"ipython==8.23.0",
|
||||
"pyfakefs==5.3.5",
|
||||
"pyfakefs==5.4.1",
|
||||
"pylint-django==2.5.5",
|
||||
"pylint==3.1.0",
|
||||
"pytest-cov==5.0.0",
|
||||
@@ -75,8 +76,8 @@ dev = [
|
||||
"pytest-icdiff==0.9",
|
||||
"pytest-xdist==3.5.0",
|
||||
"responses==0.25.0",
|
||||
"ruff==0.3.5",
|
||||
"types-requests==2.31.0.20240402",
|
||||
"ruff==0.3.7",
|
||||
"types-requests==2.31.0.20240406",
|
||||
"freezegun==1.4.0",
|
||||
]
|
||||
|
||||
|
||||
@@ -33,4 +33,3 @@ yarn-error.log*
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
next-env.d.ts
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
/// <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.
|
||||
@@ -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": "next lint",
|
||||
"lint": "tsc --noEmit && 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.28.9",
|
||||
"@tanstack/react-query": "5.29.0",
|
||||
"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.28.10",
|
||||
"@tanstack/react-query-devtools": "5.29.0",
|
||||
"@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.73",
|
||||
"@types/react": "18.2.74",
|
||||
"@types/react-dom": "*",
|
||||
"dotenv": "16.4.5",
|
||||
"eslint-config-people": "*",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 15 KiB |
@@ -34,25 +34,22 @@ describe('fetchAPI', () => {
|
||||
});
|
||||
|
||||
it('logout if 401 response', async () => {
|
||||
const mockReplace = jest.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: {
|
||||
replace: mockReplace,
|
||||
},
|
||||
useAuthStore.setState({
|
||||
authenticated: true,
|
||||
userData: { id: '123', email: 'test@test.com' },
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
expect(useAuthStore.getState().userData).toBeUndefined();
|
||||
await Promise.all([fetchMock.flush()]);
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith(
|
||||
'http://some.api.url/api/v1.0/authenticate/',
|
||||
);
|
||||
expect(fetchMock.lastUrl()).toEqual('http://some.api.url/api/v1.0/logout/');
|
||||
|
||||
const { userData, authenticated } = useAuthStore.getState();
|
||||
expect(userData).toBeUndefined();
|
||||
expect(authenticated).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { login, useAuthStore } from '@/core/auth';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
|
||||
/**
|
||||
* Retrieves the CSRF token from the document's cookies.
|
||||
@@ -29,12 +29,8 @@ 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;
|
||||
|
||||
@@ -23,6 +23,10 @@ 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 {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { PropsWithChildren } from 'react';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { HEADER_HEIGHT, Header } from '@/features/header';
|
||||
import { Menu } from '@/features/menu';
|
||||
|
||||
export function MainLayout({ children }: { children: React.ReactNode }) {
|
||||
export function MainLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<Box $height="100vh" $css="overflow:hidden;">
|
||||
<Header />
|
||||
|
||||
@@ -10,7 +10,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
||||
|
||||
useEffect(() => {
|
||||
initAuth();
|
||||
}, [initAuth]);
|
||||
}, [initAuth, authenticated]);
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './getMe';
|
||||
export * from './types';
|
||||
export * from './getMe';
|
||||
export * from './logout';
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
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 } from './api';
|
||||
import { User, getMe, logout } from './api';
|
||||
|
||||
export const login = () => {
|
||||
window.location.replace(
|
||||
@@ -30,11 +30,12 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
set({ authenticated: true, userData: data });
|
||||
})
|
||||
.catch(() => {
|
||||
// todo - implement a proper login screen to prevent automatic navigation.
|
||||
login();
|
||||
});
|
||||
},
|
||||
logout: () => {
|
||||
set(initialState);
|
||||
void logout().then(() => {
|
||||
set(initialState);
|
||||
});
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
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 IconDesk } from './assets/icon-desk.svg?url';
|
||||
import { default as IconApplication } from './assets/icon-application.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,42 +55,17 @@ export const Header = () => {
|
||||
alt={t('Freedom Equality Fraternity Logo')}
|
||||
/>
|
||||
<Box $align="center" $gap="1rem" $direction="row">
|
||||
<Image priority src={IconDesk} alt={t('Desk Logo')} />
|
||||
<Image priority src={IconApplication} alt={t('Equipes Logo')} />
|
||||
<Text className="m-0" as="h2" $theme="primary">
|
||||
{t('Desk')}
|
||||
{t('Equipes')}
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<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 $align="center" $gap="1rem" $justify="flex-end" $direction="row">
|
||||
<AccountDropdown />
|
||||
<LanguagePicker />
|
||||
<ApplicationsMenu />
|
||||
</Box>
|
||||
</Box>
|
||||
</StyledHeader>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 6.9 KiB |
@@ -1,22 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
@@ -0,0 +1,51 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './MailContent';
|
||||
export * from './MailLayout';
|
||||
@@ -0,0 +1 @@
|
||||
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 } from '@/features/teams';
|
||||
import { Role, Team } from '@/features/teams';
|
||||
import { AppWrapper } from '@/tests/utils';
|
||||
|
||||
import { MemberAction } from '../components/MemberAction';
|
||||
@@ -21,6 +21,11 @@ const access: Access = {
|
||||
} as any,
|
||||
};
|
||||
|
||||
const team = {
|
||||
id: '123456',
|
||||
name: 'teamName',
|
||||
} as Team;
|
||||
|
||||
describe('MemberAction', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.restore();
|
||||
@@ -28,7 +33,7 @@ describe('MemberAction', () => {
|
||||
|
||||
it('checks the render when owner', async () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.OWNER} teamId="123" />,
|
||||
<MemberAction access={access} currentRole={Role.OWNER} team={team} />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
@@ -41,7 +46,7 @@ describe('MemberAction', () => {
|
||||
|
||||
it('checks the render when member', () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.MEMBER} teamId="123" />,
|
||||
<MemberAction access={access} currentRole={Role.MEMBER} team={team} />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
@@ -54,7 +59,7 @@ describe('MemberAction', () => {
|
||||
|
||||
it('checks the render when admin', async () => {
|
||||
render(
|
||||
<MemberAction access={access} currentRole={Role.ADMIN} teamId="123" />,
|
||||
<MemberAction access={access} currentRole={Role.ADMIN} team={team} />,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
},
|
||||
@@ -70,7 +75,7 @@ describe('MemberAction', () => {
|
||||
<MemberAction
|
||||
access={{ ...access, role: Role.OWNER }}
|
||||
currentRole={Role.ADMIN}
|
||||
teamId="123"
|
||||
team={team}
|
||||
/>,
|
||||
{
|
||||
wrapper: AppWrapper,
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
|
||||
@@ -25,6 +26,7 @@ export const Menu = () => {
|
||||
>
|
||||
<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" />
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 854 B |
@@ -36,11 +36,13 @@
|
||||
"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",
|
||||
@@ -54,11 +56,13 @@
|
||||
"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...",
|
||||
@@ -67,8 +71,6 @@
|
||||
"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",
|
||||
|
||||
@@ -18,8 +18,8 @@ export default function App({ Component, pageProps }: AppPropsWithLayout) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t('People')}</title>
|
||||
<meta name="description" content={t('People Description')} />
|
||||
<title>{t('Equipes')}</title>
|
||||
<meta name="description" content={t('Equipes Description')} />
|
||||
<link rel="icon" href="/favicon.ico" sizes="any" />
|
||||
</Head>
|
||||
<AppProvider>{getLayout(<Component {...pageProps} />)}</AppProvider>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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;
|
||||
@@ -39,9 +39,7 @@ export const createTeam = async (
|
||||
await page.getByText('Team name').fill(randomTeams[i]);
|
||||
await expect(buttonCreate).toBeEnabled();
|
||||
await buttonCreate.click();
|
||||
await expect(
|
||||
panel.locator('li').nth(0).getByText(randomTeams[i]),
|
||||
).toBeVisible();
|
||||
await expect(panel.locator('li').getByText(randomTeams[i])).toBeVisible();
|
||||
}
|
||||
|
||||
return randomTeams;
|
||||
|
||||
@@ -17,12 +17,12 @@ test.describe('Header', () => {
|
||||
header.getByAltText('Freedom Equality Fraternity Logo'),
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Desk Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Desk')).toHaveCSS(
|
||||
await expect(header.getByAltText('Equipes Logo')).toBeVisible();
|
||||
await expect(header.locator('h2').getByText('Equipes')).toHaveCSS(
|
||||
'color',
|
||||
'rgb(0, 0, 145)',
|
||||
);
|
||||
await expect(header.locator('h2').getByText('Desk')).toHaveCSS(
|
||||
await expect(header.locator('h2').getByText('Equipes')).toHaveCSS(
|
||||
'font-family',
|
||||
/Marianne/i,
|
||||
);
|
||||
@@ -32,12 +32,24 @@ test.describe('Header', () => {
|
||||
).toBeVisible();
|
||||
|
||||
await expect(header.getByAltText('Language Icon')).toBeVisible();
|
||||
await expect(header.getByText('My account')).toBeVisible();
|
||||
});
|
||||
|
||||
await expect(header.getByText('John Doe')).toBeVisible();
|
||||
await expect(
|
||||
header.getByRole('img', {
|
||||
name: 'profile picture',
|
||||
}),
|
||||
).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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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('test1');
|
||||
await inputSearch.fill('test');
|
||||
await page.getByRole('option', { name: users[1].name }).click();
|
||||
|
||||
// Select email
|
||||
|
||||
@@ -10,6 +10,7 @@ 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 },
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"test:ui": "yarn test --ui"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "1.42.1",
|
||||
"@playwright/test": "1.43.0",
|
||||
"@types/node": "*",
|
||||
"eslint-config-people": "*",
|
||||
"typescript": "*"
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
"i18n:test": "yarn I18N run test"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/node": "20.12.2",
|
||||
"@types/react-dom": "18.2.23",
|
||||
"typescript": "5.4.3"
|
||||
"@types/node": "20.12.5",
|
||||
"@types/react-dom": "18.2.24",
|
||||
"typescript": "5.4.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@next/eslint-plugin-next": "14.1.4",
|
||||
"@tanstack/eslint-plugin-query": "5.28.6",
|
||||
"@tanstack/eslint-plugin-query": "5.28.11",
|
||||
"@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": "27.9.0",
|
||||
"eslint-plugin-jest": "28.2.0",
|
||||
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||
"eslint-plugin-playwright": "1.5.4",
|
||||
"eslint-plugin-prettier": "5.1.3",
|
||||
|
||||
+117
-108
@@ -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.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/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/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.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.3.tgz#568864247ea10fbd4eff04dda1e05f9e2ea985c3"
|
||||
integrity sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==
|
||||
version "7.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.24.4.tgz#1f758428e88e0d8c563874741bc4ffc4f71a4717"
|
||||
integrity sha512-MBVlMXP+kkl5394RBLSxxk/iLTeVGuXTV3cIDXavPpMMqnSnt6apKgan/U8O3USWZCWZT/TbgfEpKa4uMgN4Dg==
|
||||
dependencies:
|
||||
"@ampproject/remapping" "^2.2.0"
|
||||
"@babel/code-frame" "^7.24.2"
|
||||
"@babel/generator" "^7.24.1"
|
||||
"@babel/generator" "^7.24.4"
|
||||
"@babel/helper-compilation-targets" "^7.23.6"
|
||||
"@babel/helper-module-transforms" "^7.23.3"
|
||||
"@babel/helpers" "^7.24.1"
|
||||
"@babel/parser" "^7.24.1"
|
||||
"@babel/helpers" "^7.24.4"
|
||||
"@babel/parser" "^7.24.4"
|
||||
"@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.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==
|
||||
"@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==
|
||||
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":
|
||||
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==
|
||||
"@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==
|
||||
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.1":
|
||||
version "7.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.24.1.tgz#183e44714b9eba36c3038e442516587b1e0a1a94"
|
||||
integrity sha512-BpU09QqEe6ZCHuIHFphEFgvNSrubve1FtyMton26ekZ85gRGi6LrTF7zArARp2YvyFxloeiRmtSCq5sjh1WqIg==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/template" "^7.24.0"
|
||||
"@babel/traverse" "^7.24.1"
|
||||
@@ -263,10 +263,18 @@
|
||||
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":
|
||||
version "7.24.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.1.tgz#1e416d3627393fab1cb5b0f2f1796a100ae9133a"
|
||||
integrity sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg==
|
||||
"@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/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.24.1":
|
||||
version "7.24.1"
|
||||
@@ -478,10 +486,10 @@
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
@@ -493,12 +501,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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@babel/helper-create-class-features-plugin" "^7.24.1"
|
||||
"@babel/helper-create-class-features-plugin" "^7.24.4"
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
"@babel/plugin-syntax-class-static-block" "^7.14.5"
|
||||
|
||||
@@ -842,12 +850,12 @@
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
"@babel/plugin-transform-typescript@^7.24.1":
|
||||
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==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/helper-annotate-as-pure" "^7.22.5"
|
||||
"@babel/helper-create-class-features-plugin" "^7.24.1"
|
||||
"@babel/helper-create-class-features-plugin" "^7.24.4"
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
"@babel/plugin-syntax-typescript" "^7.24.1"
|
||||
|
||||
@@ -883,14 +891,15 @@
|
||||
"@babel/helper-plugin-utils" "^7.24.0"
|
||||
|
||||
"@babel/preset-env@^7.20.2":
|
||||
version "7.24.3"
|
||||
resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.24.3.tgz#f3f138c844ffeeac372597b29c51b5259e8323a3"
|
||||
integrity sha512-fSk430k5c2ff8536JcPvPWK4tZDwehWLGlBp0wrsBUjZVdeQV6lePbwKWZaZfK2vnh/1kQX1PzAJWsnBmVgGJA==
|
||||
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==
|
||||
dependencies:
|
||||
"@babel/compat-data" "^7.24.1"
|
||||
"@babel/compat-data" "^7.24.4"
|
||||
"@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"
|
||||
@@ -917,9 +926,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.1"
|
||||
"@babel/plugin-transform-block-scoping" "^7.24.4"
|
||||
"@babel/plugin-transform-class-properties" "^7.24.1"
|
||||
"@babel/plugin-transform-class-static-block" "^7.24.1"
|
||||
"@babel/plugin-transform-class-static-block" "^7.24.4"
|
||||
"@babel/plugin-transform-classes" "^7.24.1"
|
||||
"@babel/plugin-transform-computed-properties" "^7.24.1"
|
||||
"@babel/plugin-transform-destructuring" "^7.24.1"
|
||||
@@ -1006,9 +1015,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.1"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.1.tgz#431f9a794d173b53720e69a6464abc6f0e2a5c57"
|
||||
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
|
||||
version "7.24.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd"
|
||||
integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
@@ -1830,12 +1839,12 @@
|
||||
resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31"
|
||||
integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
playwright "1.42.1"
|
||||
playwright "1.43.0"
|
||||
|
||||
"@react-aria/breadcrumbs@^3.5.11":
|
||||
version "3.5.11"
|
||||
@@ -3014,36 +3023,36 @@
|
||||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^6.20.0"
|
||||
|
||||
"@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-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-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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-devtools" "5.28.10"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@tanstack/query-core" "5.28.9"
|
||||
"@tanstack/query-core" "5.29.0"
|
||||
|
||||
"@tanstack/react-table@8.13.2":
|
||||
version "8.13.2"
|
||||
@@ -3235,10 +3244,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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
@@ -3252,10 +3261,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.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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
@@ -3266,10 +3275,10 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@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==
|
||||
"@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==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.0.2"
|
||||
@@ -3442,7 +3451,7 @@
|
||||
"@typescript-eslint/typescript-estree" "7.5.0"
|
||||
semver "^7.5.4"
|
||||
|
||||
"@typescript-eslint/utils@^5.10.0", "@typescript-eslint/utils@^5.58.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==
|
||||
@@ -3456,7 +3465,7 @@
|
||||
eslint-scope "^5.1.1"
|
||||
semver "^7.3.7"
|
||||
|
||||
"@typescript-eslint/utils@^6.20.0":
|
||||
"@typescript-eslint/utils@^6.0.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==
|
||||
@@ -4018,9 +4027,9 @@ camelize@^1.0.0:
|
||||
integrity sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==
|
||||
|
||||
caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587:
|
||||
version "1.0.30001605"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001605.tgz#ca12d7330dd8bcb784557eb9aa64f0037870d9d6"
|
||||
integrity sha512-nXwGlFWo34uliI9z3n6Qc0wZaf7zaZWA1CPZ169La5mV3I/gem7bst0vr5XQH5TJXZIMfDeZyOrZnSlVzKxxHQ==
|
||||
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==
|
||||
|
||||
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0:
|
||||
version "4.1.2"
|
||||
@@ -4666,9 +4675,9 @@ eastasianwidth@^0.2.0:
|
||||
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
|
||||
|
||||
electron-to-chromium@^1.4.668:
|
||||
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==
|
||||
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==
|
||||
|
||||
emittery@^0.13.1:
|
||||
version "0.13.1"
|
||||
@@ -4990,12 +4999,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@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==
|
||||
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==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^5.10.0"
|
||||
"@typescript-eslint/utils" "^6.0.0"
|
||||
|
||||
eslint-plugin-jsx-a11y@6.8.0, eslint-plugin-jsx-a11y@^6.7.1:
|
||||
version "6.8.0"
|
||||
@@ -7412,17 +7421,17 @@ pkg-dir@^4.2.0:
|
||||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
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-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@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==
|
||||
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==
|
||||
dependencies:
|
||||
playwright-core "1.42.1"
|
||||
playwright-core "1.43.0"
|
||||
optionalDependencies:
|
||||
fsevents "2.3.2"
|
||||
|
||||
@@ -8738,10 +8747,10 @@ typed-array-length@^1.0.6:
|
||||
is-typed-array "^1.1.13"
|
||||
possible-typed-array-names "^1.0.0"
|
||||
|
||||
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==
|
||||
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==
|
||||
|
||||
unbox-primitive@^1.0.2:
|
||||
version "1.0.2"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
djangoSecretKey: ENC[AES256_GCM,data:XKgM2zd1+/bxvdUzBp3zK0XUPDPjDwsfDa6WPVUULqxZ0RcuPsKRMjBgFvE9hLd4AvY=,iv:0Uk4QXWiAW6HW/7kmx2hbwU3sEdjIsTxm3T5U0wlbws=,tag:9I+fEYxkLClZ39x1eNP7sQ==,type:str]
|
||||
djangoSecretKey: ENC[AES256_GCM,data:06KBEHV/gBgGoB4DXf9yTU5XK1xP9OXfyKEiSdSghV8XIMon3o1ajSWN+WNMRHkRZuU=,iv:ZeP1X4pQF9fVm7quzzVXSm2CSLrqAizwZD5QFmNOoSc=,tag:Dm/b+6CfznSC+CdKj1SCYA==,type:str]
|
||||
oidc:
|
||||
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]
|
||||
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]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
@@ -11,59 +11,59 @@ sops:
|
||||
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyTjRhME5rSXU4bGlzVXBu
|
||||
ZS9WanpqRU1zUVJxM01Ld0NiYVR1ak5OZ2dJClRSSTJNSTdoTEQ2UzRVSlhRbDRx
|
||||
NWxId2tsbUhFd0lOUUY4dTJTOG5tMW8KLS0tIDRpcThPaTkyQ005aXhqSnVTYkN2
|
||||
LzhKU1FUeklTd1RuUk1lSVYwK3VLTEEKcKHaluWQ+Wgs9qI0qvyfnx+goSymL9wc
|
||||
bJ0lxptRr0PGHdKhBRRlSe6HCMfshIoTktooUT6vNv4AsPmZuJZhJg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArdENIcUkwdjFsQlJubW0z
|
||||
Y2dPeStzVnRjcVlPcjJLQlFIdjBQRnVFcFJRCmU5OXRUQldIYXNWaG1IeVluVXJh
|
||||
aHowNUMvRlFHZ2J0TE50K2pMOTJBMWsKLS0tIEFwdVo5djJURU16aUdMeEhFeUsy
|
||||
QTA5bjZFWTIyeG00ZDVTbVY0UWN3WGMKReL4f5v41eEIogPSqMuiSVml1stAAAf3
|
||||
nedjWc5s2C5mO3IB+iU7uOWF6P5kIrXU4Tvmwju2E8yw4v2lmsfZLg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB5bEFSaS9TWmJ6MkRWd09J
|
||||
TVdGTGN4SS9kVE5yMlRTUHBWK09pYVZpUTFVClJmZ1pVSm1XZFIxeXkrb2gxbTYy
|
||||
eDVvT2RUNHBOWDRSa1Y1MkpxMGhzbDAKLS0tIEl2ZE1Bb0U4NGZ3QVg2ZUpRQ0o5
|
||||
YUFNOS8xUnlKOXZSZ0M2ai8yNmNxTGsKHhwRXY18pGLitckX5vxFRJyqVL4VgWbw
|
||||
+Gy+IwB7fJXoYlKHJXFfLOfhifCvrgouTcqV0ckPx/WYWSUKNDO89A==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDd2JvVHJUNXc0eGc1TTJi
|
||||
OGhjSDFkaC93dG9EWDF0WThoQWV2SjE5S25FCkdBRU55MElTdHZnUmU1ZGF5b1gy
|
||||
aFdyZGJyUzFpQVFRTVBReXp6MXZWbncKLS0tIHZaYmRLeld3UHdwWjc0WGNBQ1k4
|
||||
VkMyN1FNNysxc2RMTzlOSGlzd1RSazgKXBumJC7hLOJ3rcG2x80L/mEPGMbWKGbG
|
||||
En66KslOsgX/LugQmRey82ezDhqhnvpHe+sLWRaf9JfM+zCRg4mUMQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
|
||||
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwQnNWTC9TUHZZZ3JoUnBL
|
||||
Vmh4OEwvNzVzUS9HVVpaT3lBWlllbFZsSzJnCmhCR0NDT0Q5ekFCSDdoclB4Sml3
|
||||
ZjVnV1BpTkhmS0FUSDZmWGk4WGRrVGMKLS0tIGFCTUk1dzBaV1VBR0pLUGJtWDJh
|
||||
RVd4K2Q0b2Vqc2F6b1hmQng1RHRheWcKOHUOZm+GjvHOKI3VRlgPeH3IKojGB9F4
|
||||
YhkW83lF1Wl0XYnHEUya25bMSYLzQHOPiy2I7n4K45uk8hKQmrKE5w==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyZW85bDNRUzg2QnZKczly
|
||||
VXJiRlFLVVJDSk91bjV5ZU5HMzVjWCtnSHpVCllheVh0WE82NlQvTXNwak5mK05n
|
||||
aFlDNXM5Smw4dHFtSHRnSitUN1hhYWcKLS0tIGQ2akhocXArbCs2ZlhCU1RjUEE2
|
||||
aFZoRE5DRC96bTVqWkZ1VmV6TjJjZzAKXfP/7E4bjSoPRENvk0gThEaNuJUgukwR
|
||||
jpa5By90xamqzIRXSmnrNX20owfWugzzuAUjdE9/kiSz5R6Csi3LuQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQbTAxQnJzc2xVZUh0VFBh
|
||||
RUZiN3l6eHNLYzUyVVdqUVl5WjFBRW9uamhVCkRJM1hGWndXclNoVlMvcjQvZTlm
|
||||
K3RxL2xrb0txZk1XaGlHTGRZbVFQemMKLS0tIFZaRkFQWURzcnRaV1lqTGhMTFp0
|
||||
TTVNU2NnLzhlR0dTLzBkdThpeURWL28KHxERu5qGbXlZnTw9bHHe7AgCOZ3PI99R
|
||||
91bVqvche0QPiESnu0Od4sIHID5g5F5+EBw53lQgjEx0c4Q1GFQfFQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA2L1JKWlozOUpvTjBkNjRX
|
||||
V3hBYzdLT3k0dVBGSFJLY2crQUxEeUd1SEN3CkEzM3ZRQm93SnFiTmlianM2VUdL
|
||||
SVdpTm1DNHVRUlU0Mkd4eUxlMzFrSTQKLS0tIGZ1STFYQjlSc2dpNWVBK0Z0Z2g1
|
||||
SlhoUEtZcE5PbTJCM2haME1vR25QelUKmdhCrRs1RzWIx/1Zjmas50oFkGjjhlvD
|
||||
m5gLBMs6VSe871DczImP/l5ViqCg9w83ZYZI0c2Usn+9i016HOnFBg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBnQXlGVEJDTDlvTDI1RXAy
|
||||
ZkZSN2NzVHNmb3U3N2xRQ3EyWmtaYmZyMEM0Cjh0QWh2ZUZycFVjbEJmdDNEK0pZ
|
||||
MGFSTW13TkRmU1RkZzVvVTZpQkMvaW8KLS0tIE5JRUtOSHErSmtnN1krZFVEd2hs
|
||||
OXNMaEc4ZUw0RW9qaHhiUVdIZVZrVlEKMBG7NyFXqT6zxwxIq30Nj+uWz/zhjbhU
|
||||
y4JqomFHxzwySEQD/1rfnTIJpmgpJNbyvRo4ToLDsM3B8TWk6D7/MQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBQbWVvaWpQZE1CdXFjMVVi
|
||||
SFFwSmRxTFdxUGtpQWZNNXlWWDQ1cXkzWTFNCm5WRlN0dVlFVW9ONXJQb2lic0oz
|
||||
dXhMSk1RN25qT2VXZGkyVmY3TTJvT1UKLS0tIHBjK293bnRhLzRCOU9hNXQ4MVNN
|
||||
K0ErNEhLNWFoc0hXdTE3MnBqT2pLblkKx9ww+qLJKdikom59GGth8/lWWmzKS2k+
|
||||
d+4votCaQYJtQbBuHUcKAKUeKFl0jBMJPoRO4XodrprXHtpU1l+nUg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArMnFJMXR3SFBuTGt2TWdL
|
||||
bUZMQkU3dTlYS0xqbW9xN3JrY28rWEtIcHpFCmUxbFBQYWgyM1dPOUtLVmEwWDJj
|
||||
N1RtUkVVSHFmUWkyZFBndmhGeFgwc1kKLS0tIHNMSjVYemQyTUlqVGVtVlBHU2cx
|
||||
eEh1MmhQRFNyNE1NSDdwWk5BRCtDMFUKZByCL2Wj0X+lwUo06PHwOiaJhzqOMVVt
|
||||
Rj/pvynxLV4d0RBzwpgdL9uV8VzTED4GW9wotODbhEUtdlpSS1YOGg==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAyOUtTRU5RaEVUaVQvM3Ro
|
||||
L3FhR3NhK1lHNFI1TjlBUGJCOXowR3F0VnprCnBNL1ZKbHJkcEZhbWpTQzFIUnBX
|
||||
NmxjTDNCRmVhZnNOM1pwRGdTZTBYZk0KLS0tIDVIcUF4MHNlVXBKVnBGSk1vd3JD
|
||||
OXBHekx1RlpSYlFnYld3T2Nza0R5bmsKt4mBjr+YP/li9Wq6GL5eJBGrSBi2GcE7
|
||||
GjP1pYyt0nsazuRrueKXWE12p4JWz0CI7vUsLfrxd9JiEdrPuC9hrA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-03-26T22:31:10Z"
|
||||
mac: ENC[AES256_GCM,data:OujhtajsuAQrC5KNFPxqjMlHAS9tpjvvsu8LRZf8XKrMui3ZBAHO2TdF7z/sAEB9OPlPJZGZU4jMDNXZkIi1zv3mUDNJXPs0oitgIEXSYCDHcZQea093hSMd4tX1yLQM3M5GH1aFZDXfIpKT7UvLjiVv+8aXp7BLQNNilbFKV1g=,iv:1OD4SuMUSD3fcuO4QiZpjij49JHwKqDJvXOT0wyJ0zs=,tag:VW4KiInBhIpbaaAS18eNUQ==,type:str]
|
||||
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]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
djangoSecretKey: ENC[AES256_GCM,data:trxlec0na+q/9kh7kkZpuyp1Zjx9uXyL3fS+UEsGsN612X5tVLtAUuSx8aGYr/f6uYs=,iv:+gjpSUBq9TEmeLYC18vT2HOi869W9xgzxC5QchI1iUg=,tag:zvKVaqwY43bRWOhhr3dFcg==,type:str]
|
||||
djangoSecretKey: ENC[AES256_GCM,data:dVq/508Au7M/Z0KqVKfaAQ1Qv0NR9EixneJXgcQLYPqr1zALAs8YdTfAHO97ObkYguM=,iv:TDVByohsak3njekbj7gPcYqWzBAxFAEn8Y7EpnyZiRM=,tag:Qfsp/PTbJghPNsJJVf5mnQ==,type:str]
|
||||
oidc:
|
||||
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]
|
||||
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]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
@@ -11,59 +11,59 @@ sops:
|
||||
- recipient: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1SzRHcERkUUdLVk9XOG9M
|
||||
MDdjSGZDam9wbUNwMFZaZ0I0bXJZSDRhTUJVClh3YkRXR3lSRGNrditwejZaeWVD
|
||||
MVIxNG0yMjNzNlVNVFc2N0Y0dXlaSkkKLS0tIHpWcGZNRkoxZDJJcDg0Y1hJOWM4
|
||||
YTdVVC8xU0p1RTZMTmFSQ20rdGFydGsKb/iZA5lO/QdPnNIuC3irxT2Ajh4C5SES
|
||||
p74VU20kUNFt7WsHMUBlkxbC2p4Mw+qacjIGqpezC+69UlSwTXawMA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRYy90Z3ZxbjdldnRoaDJ2
|
||||
VHlHeDNQVkY5ejZ0Y0F0NGJ2cE9uNlRkVHdBCkQ4ejdSZmxEWmpodDRvcGFTa2ND
|
||||
VlpXL2lGUVJncHZURSttbEw4cC9WekkKLS0tIHhrWFpCRDJvNkNOYWZzYnVGb2l2
|
||||
M3NoaGpVSlF0N1k2UXNVNFRTWTlNa0EKaGkcGVgeJFTv844UQ6tBY5hT18PoRhh4
|
||||
uIL6bH2Bs6P+wIbmuqwKhba8muS9rWbvFJppD8N/htJT2ZzXgmZAvQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBzVjZTdENzQllqayt4L1RE
|
||||
aWFTaHQ0MXpCWFIxN1ZySmFIRXlzSEZrM0c0CmpSUXFISGF1S3IyMjNIdEdSTEJs
|
||||
MW0rQmdRRWxVREU0c1dUWnpNQW9kbHMKLS0tIE9SVUFERk9CT2RDSmdjYjlzUnNm
|
||||
MTJGQTRZTzQzeEVrVnFxZVErdWpKMVkKMZzombPphRq0mEKxQotJfLBdBQz+PDJU
|
||||
YbiTe9jLLWeNDdoqMKNmcAtW0tBL0r3KWtGIZRWDV+IXXXbkVRubnA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAxUkhaS0lEbGRrRkxpa2wz
|
||||
VUlRMFFYbVJpa0tJSjBKUGlPVXE4NW5XUmdrCjV0SUVTNUJCTXRnbEpIMm44N05L
|
||||
R1pWTWVZZzZHQ0U2ZGVQdk9kMmpZUncKLS0tIFJhZ2V1aCtYTHJWNFZ3bWpibTBs
|
||||
QWJ0ajN1U3NjVHVjTE9HWnRVOWdyWEUK+Fu4p4oAwAH5nhaWKo6C/MhdAo7IbkAt
|
||||
qarRcXRIRlr29K4IpmbbiIZZA/e1uWxMxD1Bafj4pIFppKTQFeIkSQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1g5keveae6zhn059e7cxkjqdz4v3u47ypejv9ujld65nwh6d5pd9qfm0ecv
|
||||
- recipient: age1plkp8td6zzfcavjusmsfrlk54t9vn8jjxm8zaz7cmnr7kzl2nfnsd54hwg
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAzSnJrUTFiZm0vRldkS2hr
|
||||
cUl6bmpHbDBtTmJRWnp1YTR1SzZFMmZmc3dnClVra25QNnBURkZySGRuZlFHNWk2
|
||||
dUhvNVhqaWU1bVZVQlpyeDh4eXo3M0EKLS0tIFJHbXU3eG5velpZSTdESysvcVFr
|
||||
MzBjM0plTnZGakhqckJ4L0NVVGVBSEkK3FI3omG4PXTmBxnnUVAwyRA2B99rzbtx
|
||||
GqqSqCFYfn4aSFz6kz4+hxzv9rEgMBWhqpA6dpfBbz3SxmbDTW8V6Q==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWmFBWWhtRlR4emtWRXhG
|
||||
M2Z2MVZVWFpETU9BTlRZSXljMzNsa3E1dlVnCjdWeXhLYXNPdCs3R2FTK2tiK0VD
|
||||
WGc5cWYyUEtvMmVJbTRPZ25zdDNzd2cKLS0tIFNJWnd5c2tQZkwrdGx6UE1jOHpO
|
||||
L0hlY0NLdS9FVk5FdW1md2lmU0lpQmMKZ4vZhT4Fmii9HHhJ+W9/BUkmzmzXnMHg
|
||||
q8jk+pDfNR9P8Lw+95Q8DjV6uvLpw9XjOkQzm6UCNKk9/M17c4EHeQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age12g6f5fse25tgrwweleh4jls3qs52hey2edh759smulwmk5lnzadslu2cp3
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSA5SEZlcnNDRlNSTWRrdG1S
|
||||
KzRMZ2RBdzVEQVBwTFNXRVk2dG5DUnc0MjJnCnVoeVZ2WnRVaWFMMjBESTZUeHQ0
|
||||
RllWdnNXRTRkbWZXbldVVHc0QldhQkkKLS0tIDhuSU1sNXU5R2p5LzI1YXp2VEVo
|
||||
M3JqeGd2MnQ0QnNNM0cyWHpUYmlHc1EKnZazjekMiytOi1jLktn9DoaRHT0lQP2s
|
||||
GYvHZ4+xM3LwobmnVJCq1bXnl8fBuVKZbTOG+WeJbxNJq9fSk2I6rw==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBDbENsZjZsTlp2ZXo5em5O
|
||||
U0ZaZWtkZjFFK0I0L3pFZDM2c3dUS09abDJrClV3ejFjU1NwZzZZaEhqNFUvQVNL
|
||||
K08xMm1pR3dTOHZyY0dYSlo0TG9iRm8KLS0tIGc2ZVBzRzV1WW03VUQ3ZU4wVGZn
|
||||
YnNmL1pyQk4ySVMxbXh5V1pGdDlaTHcK4R15lD5ryKO7CvgpOGmfSu8i7lbkT9EI
|
||||
lWC+AXSfKmhAZzXihrgmANcoIk4zitjHOoJN/PK9DAZSskhBqbm8qA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1tl80n23wq6zxegupwn70ew0yp225ua5v4dk800x7g2w6pvlxz46qk592pa
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBRSDRQRlc2dUVPWjN4Tlpw
|
||||
cGpXeGtxc1JWOVdSc0s1WGRrTUl0UmcyVmhrCmNqWkZuZDJYLzM5YlhuVFc0bEln
|
||||
aExWZ0F6bnBzYkd4V0xKeFZudHplL1UKLS0tIHUvSUtadlRCeC8yNUVEVjZEVm1L
|
||||
RXVINFI5bHdDWGNjUjNsRU8xbTd0T0UKYL3phOso3YNi6tTWbpHdXW/Pae6uzz17
|
||||
AmLjdjD4KUVTlu6bhzSrazP+3EDtO5X3S57nladHcvxQPdqgAJQ99A==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqMEJTOG5Jb2pjR3NaQVhV
|
||||
WkQ1VnY2Ny9UcDlpcTZnc3FFQkRCRlJvY0I4CmNEMjE1Rm9KZ3BzaHhMUWpFczlK
|
||||
K1JtNlZMcno4cEROMHpYd0R1MC95QzQKLS0tIHBBdWRGRVFyME1tU1hrUk9Ha2pH
|
||||
eEx6Z2VHSHZOTFZhdWtVVUJWTGpObDgK8MB5SYG4oJswJEqWa274FK6YXlMoFO0k
|
||||
cGibj3uCo4XWaHdV3ik9GrKg68yo3yrgsc7pyB8aSHfgs47teO6Qhg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
- recipient: age1qy04neuzwpasmvljqrcvhwnf0kz5cpyteze38c8avp0czewskasszv9pyw
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtV3JvKzZnaGN4WTBsTnc2
|
||||
TDNNYk10ZWhOWHAyZjlWWGRsMDJuSHI3cXg0CllVL1VIVitSLzNieEc2ZWppdVFi
|
||||
RmlZMjE2cWdEcVhBenl2UG5McUZUK0UKLS0tIE1XMlN0YzZWVVVOdlJsZVZ1VTVC
|
||||
bnBRVTJYUzYzNEM1eU8vQzlqdk9lY3MKM9g8opHNjlm2cAkVzc9LXt2TM+Jmq8Of
|
||||
DbVFbegKV8lgnLKdmWVeKDtLFHiZj4dQclvwxbNuIk2QvEj9Wam7uQ==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBXakoxT0JQbEdoMUN3L216
|
||||
MmRFdFRsTDhSd0tKaUpNTTlFMUptMUtOWDI4CmFnb2hTeXIzUEluTllpbStxcVZI
|
||||
RytwdmZqeUhKVUQrK3BhUTRybEo3cDAKLS0tIDJXUWN3S0F6SXB1dU4za1IrZmYz
|
||||
WnJhTHJvZmVuT2NkZDJnMGxBMS83S1EKY6Up5cDbV4vVZLzxm6Z7r+pTRH9Gfoun
|
||||
Li7lS9Vv9WVs7yLFbJ2Iu0qEIkgkJetzMhV/bo305nai3bcZfvm1bw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2024-03-26T22:31:45Z"
|
||||
mac: ENC[AES256_GCM,data:jC6rd1YvDrDZxY4gfPGYY6rwygTgeL0w95a81aGAElthOl+r9eaNsqDvkOYaEuoZctdIP4MmaGfC8ZjBGrK+WbTacxHNmv9OaB43STrEqBmZWVDRUJZEgYhdQGMOW0jpDc08WaV67J1ViEuH4bcaUCwHrT//HQUf3e+25ZsO4R8=,iv:tf6xbLvxzURT0FcV0ZZYaT0b6v6GnIO854NLZZvCS8Q=,tag:5lz71fju3WHwQKTyW+lkAw==,type:str]
|
||||
lastmodified: "2024-04-05T09:42:43Z"
|
||||
mac: ENC[AES256_GCM,data:RHUdOrgnbTCzrcyoWKfz7qC3i81ZUIyxBzBl3xQH/kCXsVbIPhtRUvFLwgd9uhNNiiBjPfx68GwiXatSko8vPf0rj2FVaC+w6yf9RTItxWqGETS18Waf5etsFCMhJ4LYce79DJ8KFtqjB64VYF3BVgX9Cif7wy1jGklbN7cGgjg=,iv:sllxfa74NAQTGHuBufOS6jH7VSOu5JsvwzNfBK5QRKw=,tag:WN6vWKtiPkZdbaJ04Q/VRA==,type:str]
|
||||
pgp: []
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.8.1
|
||||
|
||||
@@ -20,6 +20,6 @@
|
||||
},
|
||||
"homepage": "https://github.com/numerique-gouv/people#readme",
|
||||
"devDependencies": {
|
||||
"openapi-typescript-codegen": "0.28.0"
|
||||
"openapi-typescript-codegen": "0.29.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.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==
|
||||
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==
|
||||
dependencies:
|
||||
"@apidevtools/json-schema-ref-parser" "^11.5.4"
|
||||
camelcase "^6.3.0"
|
||||
|
||||
Reference in New Issue
Block a user