Compare commits

...

31 Commits

Author SHA1 Message Date
lebaudantoine e502368c8b 🚧(backend) return groups when requesting the resource server
Work in progress code, used during last Monday demo.
I will need to iterate.
2024-05-31 18:15:00 +02:00
lebaudantoine e4da9b23cd 🚧(backend) refactor token introspection to separate responsibilities
Note: This commit is work in progress and has not been tested on my
machine. I'm pushing it now to gather early feedback.

Based on @Jonathan Perret's recommendations, I've split the responsibilities
of resource server authentication into three distinct classes, each mapping
to specific roles:

- A DRF authentication class
- A client to interact with the Authorization Server
- A client to interact with the Resource Server

In the OAuth 2.0 context, the Authorization Server is equivalent to the OIDC
provider. This separation aims to improve the readability of the code and
facilitate unit testing.

Additionally, this refactor provides better documentation of the technical
specifications followed by my code.
2024-05-31 18:07:52 +02:00
antoine lebaud cc157904d0 (backend) implement token introspection matching Agent Connect specs
**important** Agent Connect doesn't follow the RFC 7662, but a draft
RFC, which adds security (signing/encryption) to the initial spec.
Please take a loot at the "references" part.

Concept:

This commit initiates the implementation of token introspection by
requesting the OIDC provider (OP) to validate the access token received
from the client. The access token is initially issued by the OP, passed to
the Service Provider (SP), and then forwarded to the Resource Server (RS) by
the SP.

The token introspection is done by our custom RS authentication class.

The OP is the service capable of validating the integrity of the access
token. Token introspection requests to the OP should provide authentication
and authorization information about the user currently logged in the SP,
which requests data from the RS.

Data returned by the OP to the RS are encrypted and signed.

To encrypt the introspection token, the OP retrieves DP public key from the
newly introduced endpoint '/jwks'. The encryption parameters (algorithm and
encoding) are set while configuring the RS. Please ensure that the encryption
algorithm and encoding are matching between the OP and the RS.

Token signature is verified by the RS, using OP's public key, exposed through
its '/jwks' endpoint. Please make sure the signature algorithm are matching
between the OP and the RS.

Finally, introspection token claims are validated to follow good practices while
handling JWT. Claims as issuer, audience, or expiration time are validated.

The introspection token contains a sub. RS retrieves the requested db user from
using this sub. This should work with a paiwise or a public sub.

References:

The initial RFC describing the token introspection is the RFC 7662 "Oauth 2.0
Token Introspection". However, the introspection response specified in this RFC,
is a plain JSON object

In eGovernement applications, our resource server requires a stronger assurance
that the authorization server issued the token introspection response.

France Connect's team decided to implement a stronger version of the spec, which
returns a signed and encrypted token introspection response. This stronger
version is still draft, you can find it by searching:

"draft-ietf-oauth-jwt-introspection-response"

For documentation on the token introspection flow, refer to the France
Connect documentation at the "validation token" section. It mentions both specs
but doesn't explain their choice.

Although the documentation references France Connect, it describes the
general behavior required by the draft specification, which is also supported
in Agent Connect.
2024-05-31 18:07:52 +02:00
antoine lebaud b06e30f436 🔧(docker) integrate app-dev with the France Connect Docker network
!!! this commit will be dropped before merging, it's useful only for
dev purposes.

Enabled communication between the Desk backend and France Connect backend
by housing both within the same Docker virtual network.

Instead of creating a new network, I opted to incorporate the Desk backend
container into the existing network spawned by the France Connect Docker stack.
This approach required configuring the network as external to ensure proper
integration and functionality.

This is inspired by the suggestion from @sam, drawing insights from
the connections between Richie and Open edX.
2024-05-31 18:07:23 +02:00
antoine lebaud d124b57d16 ♻️(backend) rename Django settings related to the Resource Server (RS)
To align with the previous OIDC configuration format, this commit prefixes
every setting with "OIDC", followed by the relevant part (e.g., "RS"). This
ensures consistency across the OIDC configurations.
2024-05-31 14:38:24 +02:00
antoine lebaud bc182fe313 ♻️(backend) reorganize Django settings related to OIDC
This commit proposes a reorganization of Django settings related to OIDC
based on the OIDC parts they are associated with.

Settings are now categorized into four groups:
- OIDC Provider (OP) settings
- Resource Provider (RP) settings
- Resource Server (RS) settings
- Miscellaneous settings related to the Authorization flow

This commit is a proposition, and and may be discarded.
2024-05-31 14:38:06 +02:00
antoine lebaud be364d9227 (backend) add get_settings method
Copied from mozzilla-django-oidc repository, add a static method
on my authentication backend, which get settings from Django settings,
or give a default value if provided as an arg.

It will be use a lot when accessing Django settings in the next methods.
Might be discussed, and considered as an unnecessary wrapper.
2024-05-31 14:38:06 +02:00
antoine lebaud 681065dd85 🛂(backend) introduce an authentication class for the resource server
This commit introduces a new resource server authentication backend.
It's work in progress and will handle token introspection. It is designed to seamlessly
integrate with DRF code elements.

[explain in which context it will be useful]
2024-05-31 14:38:06 +02:00
antoine lebaud a1065e55e0 ♻️(backend) encapsulate cryptographic key loading
I explained the responsibilities of the private key in the context of the
resource server.

Consider enhancing this utility function, as it currently imposes an
opinionated approach to loading the private key and passing parameters to the
joserfc utilities.

The documentation could be more concise to avoid excessive detail.

The current API and return type are tightly coupled with joserfc usage and
types. Should we consider exposing a generic Python object to simplify the
interface?
2024-05-31 14:38:01 +02:00
antoine lebaud 6b15954d05 (backend) expose cryptographic key for encryption
This commit focuses on generating a valid keyset for the project. The following
points need to be discussed:

- Error Handling
- Settings' Names
- Storing Private Key as a decoded string or a PEM file

The JWK structure and parameters align with the implementation of France
Connect (FC) mocked data provider, named resource server in Oauth spec.

The current implementation is fully parameterizable, allowing for easy
configuration changes via Django settings. For instance, switching to another
key type is as simple as updating the settings.

However, one critique of this design is its lack of extensibility. If the
decision is made to offer more than one encryption method, this view will
require refactoring.

This commit is laying the foundation for further discussions and improvements.
2024-05-31 11:41:01 +02:00
antoine lebaud 23cf6c31f7 (backend) handle JSON Object Signing and Encryption (JOSE)
joserfc has been chosen as the dependency to implement a JWKs endpoint in the
Django app. While there are several alternatives available such as pyjwt,
python-jose, jwcrypto, etc., joserfc has been opted for the following reasons:

- Cryptography: Although using only cryptography is feasible, its
  interface/API is not as user-friendly.

- pyjwt: While pyjwt is popular, it lacks support for JWK and JWE, which are
  essential for the requirements.

- python-jose: The latest release of python-jose was in 2021, and the
  project seems less active compared to other alternatives.

- Authlib: Authlib is the second most popular library after pyjwt and seems
  modern with an active community. However, the parts relevant to the use case
  were extracted into a relatively new package named joserfc.

- joserfc: Although joserfc has fewer stars compared to Authlib, it was
  extracted from Authlib, which has more than 4k stars, indicating a solid
  foundation.

While the low star count of joserfc might raise concerns about its stability, it
is believed to be worth considering its addition. Adding Authlib and refactoring
later, once they finish migrating to joserfc, is a possibility to address
any potential regressions.

One of my concern is that joserfc hasn’t been released in a major version yet.

This commit is a work in progress, and further evaluation and testing are
needed before finalizing the integration of joserfc into the project.
2024-05-31 11:08:46 +02:00
antoine lebaud 5b2988491a 🤡(backend) mock jwks endpoint
This introduce a new endpoint, which is a read-only URL that returns JWKSs
as JSON objects in response to GET requests.

JWKSs are sets of JWKs, which are JavaScript Object Notation (JSON) data structures
that represent cryptographic keys.

This /jwks route might be refactored to the be located at a well-known location.
2024-05-31 11:08:37 +02:00
antoine lebaud 669633285c 🏗️(backend) create a new python package for the resource server
Encapsulate all Resource Server (RS) sources in a dedicated python package.
2024-05-31 10:57:23 +02:00
renovate[bot] a1f9cf0854 ⬆️(dependencies) update python dependencies 2024-04-16 10:27:16 +02:00
Lebaud Antoine 2f1805b721 🩹(backend) address linter flakiness on Email tests
Pylint was randomly failing due to a warning while unpacking emails.
The W0632 (Possible unbalanced tuple unpacking) was triggered.

Replace tuple unpacking by an explicitly accessing the first element of
the array using index.
2024-04-08 15:35:12 +02:00
renovate[bot] 711abcb49f ⬆️(dependencies) update python dependencies 2024-04-08 15:35:12 +02:00
Anthony LC e68370bfcd ⬇️(eslint-config-people) eslint 9.0.0 -> 8.57.0
Downgrade eslint to 8.57.0.
9.0.0 has breaking changes, the adoption
is still very low (1%), better to wait.
2024-04-08 15:18:17 +02:00
renovate[bot] ae1acd8840 ⬆️(dependencies) update js dependencies 2024-04-08 15:18:17 +02:00
Lebaud Antoine 54386fcdd3 🩹(backend) address test flakiness while sorting Team accesses
Previously, there was a difference between Django's `order_by`
behavior and Python's `sorted` function, leading to test failures
under specific conditions. For example, entries such as 'Jose Smith'
and 'Joseph Walker' were not consistently sorted in the same order
between the two methods.

To resolve this issue, we've ensured that sorting the expected
results in the TeamAccess tests are both case-insensitive and
space-insensitive. This adjustment fix tests flakiness
2024-04-08 15:07:58 +02:00
Anthony LC 45a3e7936d (app-desk) create mails feature
Create the archi to handle the mails feature.
It has a different layout than the other features,
we don't display the sidebar to keep the
user focused on the mail content.
2024-04-08 14:42:56 +02:00
Marie PUPO JEAMMET ebf58f42c9 (webhook) add webhook logic and synchronization utils
adding webhooks logic to send serialized team memberships data
to a designated serie of webhooks.
2024-04-05 16:06:09 +02:00
Samuel Paccoud - DINUM 7ea6342a01 ♻️(models) refactor user email fields
The email field on the user is renamed to "admin_email" for clarity. The
"email" and "name" fields of user's main identity are made available on
the user model so it is easier to access it.
2024-04-05 16:06:09 +02:00
Anthony LC 6d807113bc 🔧(sops) update secrets
Access to anthony's new key
2024-04-05 12:21:13 +02:00
Jacques ROUSSEL 5455c589ef 🔧(sops) update secrets
Decrypt and reencrypt secrets to grant access to anthony's new key
2024-04-05 09:48:19 +02:00
Lebaud Antoine 9ec7eddaed (frontend) add logout button
Rework the header based on latest Johann's design, which introduced a
dropdown menu to mange user account.

In this menu, you can find a logout button, which ends up the backend
session by calling the logout endpoint. Please that automatic redirection
when receiving the backend response were disabled. We handle it in our
custom hook, which reload the page.

Has the session cookie have been cleared, on reloading the page, a new
loggin flow is initiated, and the user is redirected to the OIDC provider.

Please note, the homepage design/organization is still under discussion, I
prefered to ship a first increment. The logout feature will be quite useful
in staging to play and test our UI.
2024-04-04 14:52:53 +02:00
Lebaud Antoine 7db2faa072 🍱(frontend) rename Desk to Equipes
Update all assets related to the previous naming.
I gave some asset a more generic name, so if the app's name chang again
we won't have to rename the logo.

Linked but not assets, meta title and description were updated.
Next.js favicon was replaced by the Equipes' one.
2024-04-04 14:52:53 +02:00
Anthony LC 6e0b329b09 🐛(app-desk) add next-env.d.ts
next-env.d.ts lot of types for the next.js.
next.js boilerplate don't version the
next-env.d.ts file, so it was being ignored by git.
This was causing the CI to fail with the ts linter.
2024-04-04 12:13:00 +02:00
Lebaud Antoine c7aae51d25 🩹(test) update MemberAction props
TeamId prop was refactored to a Team object.
Update MemberAction component's tests.
2024-04-04 12:13:00 +02:00
Anthony LC db40efb360 🚨(app-desk) improve linter
The linter was passing near the ts errors in
the tests, we improve the linter by adding
a ts checkup.
2024-04-04 12:13:00 +02:00
Anthony LC 3ddc519d9e (e2e) fix job flakinness
Fix a flaky jobs
- searched username could be hidden in the options
 depends the dummy data generated.
- remove first place assertion when create a new team,
  multiple workers make this assertion flaky
2024-04-04 12:13:00 +02:00
Lebaud Antoine e20960e3e1 💚(ci) update Github Actions using Node.js 16
Github Actions are transitioning from Node 16 to Node 20. Make sure we use
latest Github Actions versions to clean any deprecation warnings.

The migration is upcoming.
2024-04-04 10:33:20 +02:00
78 changed files with 2014 additions and 456 deletions
+11 -11
View File
@@ -16,7 +16,7 @@ jobs:
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@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'
+17 -17
View File
@@ -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
View File
@@ -2,14 +2,14 @@ creation_rules:
# Here we have
# - Jacques key-id: age15fyxdwmg5mvldtqqus87xspuws2u0cpvwheehrtvkexj4tnsqqysw6re2x
# - github-repo key-id: age16hnlml8yv4ynwy0seer57g8qww075crd0g7nsundz3pj4wk7m3vqftszg7
# - Anthony Le-Courric key-id: 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
+1 -1
View File
@@ -183,7 +183,7 @@ migrate: ## run django migrations for the people project.
superuser: ## Create an admin superuser with password "admin"
@echo "$(BOLD)Creating a Django superuser$(RESET)"
@$(MANAGE) createsuperuser --email admin@example.com --password admin
@$(MANAGE) createsuperuser --admin_email admin@example.com --password admin
.PHONY: superuser
back-i18n-compile: ## compile the gettext files
+11
View File
@@ -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
View File
@@ -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-----
"
+14 -5
View File
@@ -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",
+3 -23
View File
@@ -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."""
+2 -5
View File
@@ -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
+9
View File
@@ -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")
+13 -3
View File
@@ -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"""
+19 -2
View File
@@ -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.'),
+94 -8
View File
@@ -13,7 +13,7 @@ from django.conf import settings
from django.contrib.auth import models as auth_models
from django.contrib.auth.base_user import AbstractBaseUser
from django.core import exceptions, mail, validators
from django.db import models
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."""
+118
View File
@@ -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)
+219
View File
@@ -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
+11
View File
@@ -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()),
]
+61
View File
@@ -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
+54
View File
@@ -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
+19 -19
View File
@@ -53,8 +53,8 @@ def test_api_users_authenticated_list_by_email():
Authenticated users should be able to search users with a case-insensitive and
partial query on the email.
"""
user = factories.UserFactory(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
+25 -10
View File
@@ -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"
View File
+70
View File
@@ -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,
)
+75
View File
@@ -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
+2
View File
@@ -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),
+69 -26
View File
@@ -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):
+8 -7
View File
@@ -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",
]
-1
View File
@@ -33,4 +33,3 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+1
View File
@@ -0,0 +1 @@
next-env.d.ts
+5
View File
@@ -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.
+4 -4
View File
@@ -8,7 +8,7 @@
"build:ci": "cp .env.development .env.local && yarn build",
"build-theme": "cunningham -g css,ts -o src/cunningham --utility-classes",
"start": "npx -y serve@latest out",
"lint": "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 -5
View File
@@ -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",
+2 -2
View File
@@ -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 },
+1 -1
View File
@@ -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": "*"
+3 -3
View File
@@ -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
View File
@@ -28,23 +28,23 @@
"@babel/highlight" "^7.24.2"
picocolors "^1.0.0"
"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.23.5", "@babel/compat-data@^7.24.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"
+36 -36
View File
@@ -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
+36 -36
View File
@@ -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
+1 -1
View File
@@ -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"
}
}
+4 -4
View File
@@ -88,10 +88,10 @@ neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
openapi-typescript-codegen@0.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"