Compare commits

..

10 Commits

Author SHA1 Message Date
Anthony LC 3eff663a78 ♻️(frontend) adapt doc visibility to new api
We updated the way we handle the visibility of a doc
in the backend. Now we use a new api to update
the visibility (documents/{id}/link-configuration/)
of a doc. We adapted the frontend to use this new api.
We changed the types to reflect the new api and
to keep the same logic.
2024-09-11 10:23:06 +02:00
Anthony LC feabab030d fixup! (models/api) add link access reach and role 2024-09-10 16:07:43 +02:00
Samuel Paccoud - DINUM 4002a049eb (models) allow null titles on documents
We want to make it as fast as possible to create a new document.
We should not have any modal asking the title before creating the
document but rather show an "untitle document" title and let the
owner set it on the already created document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM c7d8211aac (api) allow forcing ID when creating a document via API endpoint
We need to be able to force the ID when creating a document via
the API endpoint. This is usefull for documents that are created
offline as synchronization is achieved by replaying stacked requests.

We do it via the serializer, making sure that we don't override an
existing document.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 25a24eaf12 🔥(api) remove possibility to force document id on creation
This feature poses security issues in the way it is implemented.
We decide to remove it while clarifying the use case.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 9adecf93ec (api) allow updating link configuration for a document
We open a specific endpoint to update documents link configuration
because it makes it more secure and simple to limit access rights
to administrators/owners whereas other document fields like title
and content can be edited by anonymous or authenticated users with
much less access rights.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 6a3a07db31 🐛(api) fix randomly failing test on document list ordering via API
The test was randomly failing because postgresql and python sorting
was not 100% consistent e.g "treatment" vs "treat them" were not
ordered the same.

Comparing each field value insteat of relying on "sort" solves the
issue and makes the test simpler.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 79b56ccd4e (models/api) add link access reach and role
Link access was either public or private and was only allowing readers.

This commit makes link access more powerful:
- link reach can be private (users need to obtain specific access by
  document's administrators), restricted (any authenticated user) or
  public (anybody including anonymous users)
- link role can be reader or editor.

It is thus now possible to give editor access to an anonymous user or
any authenticated user.
2024-09-10 16:02:25 +02:00
Samuel Paccoud - DINUM 611d77f3bb 🔥(compose) remove docker compose version
The version is now automatically guessed by Docker Compose. This
commit will fix the warning raised about the uselessness of this
setting.
2024-09-10 16:02:25 +02:00
Anthony LC de0acedc0a 🛂(backend) stop to list public doc to everyone
Everybody could see the full list of public docs.
Now only members can see their public docs.
They can still access to any specific public doc.
2024-09-10 16:02:25 +02:00
26 changed files with 12118 additions and 13585 deletions
+139 -139
View File
@@ -9,52 +9,52 @@ on:
- "*"
jobs:
# lint-git:
# runs-on: ubuntu-latest
# if: github.event_name == 'pull_request' # Makes sense only for pull requests
# steps:
# - name: Checkout repository
# uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: show
# run: git log
# - name: Enforce absence of print statements in code
# run: |
# ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
# - name: Check absence of fixup commits
# run: |
# ! git log | grep 'fixup!'
# - name: Install gitlint
# run: pip install --user requests gitlint
# - name: Lint commit messages added to main
# run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
lint-git:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: show
run: git log
- name: Enforce absence of print statements in code
run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
- name: Check absence of fixup commits
run: |
! git log | grep 'fixup!'
- name: Install gitlint
run: pip install --user requests gitlint
- name: Lint commit messages added to main
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
# check-changelog:
# runs-on: ubuntu-latest
# if: |
# contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
# github.event_name == 'pull_request'
# steps:
# - name: Checkout repository
# uses: actions/checkout@v3
# with:
# fetch-depth: 50
# - name: Check that the CHANGELOG has been modified in the current branch
# run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
check-changelog:
runs-on: ubuntu-latest
if: |
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch
run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
# lint-changelog:
# runs-on: ubuntu-latest
# steps:
# - name: Checkout repository
# uses: actions/checkout@v2
# - name: Check CHANGELOG max line length
# run: |
# max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
# if [ $max_line_length -ge 80 ]; then
# echo "ERROR: CHANGELOG has lines longer than 80 characters."
# exit 1
# fi
lint-changelog:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Check CHANGELOG max line length
run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
if [ $max_line_length -ge 80 ]; then
echo "ERROR: CHANGELOG has lines longer than 80 characters."
exit 1
fi
build-mails:
runs-on: ubuntu-latest
@@ -96,112 +96,112 @@ jobs:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
# lint-back:
# runs-on: ubuntu-latest
# defaults:
# run:
# working-directory: src/backend
# steps:
# - name: Checkout repository
# uses: actions/checkout@v2
# - name: Install Python
# uses: actions/setup-python@v3
# with:
# python-version: "3.10"
# - name: Install development dependencies
# run: pip install --user .[dev]
# - name: Check code formatting with ruff
# run: ~/.local/bin/ruff format . --diff
# - name: Lint code with ruff
# run: ~/.local/bin/ruff check .
# - name: Lint code with pylint
# run: ~/.local/bin/pylint .
lint-back:
runs-on: ubuntu-latest
defaults:
run:
working-directory: src/backend
steps:
- name: Checkout repository
uses: actions/checkout@v2
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
- name: Install development dependencies
run: pip install --user .[dev]
- name: Check code formatting with ruff
run: ~/.local/bin/ruff format . --diff
- name: Lint code with ruff
run: ~/.local/bin/ruff check .
- name: Lint code with pylint
run: ~/.local/bin/pylint .
# test-back:
# runs-on: ubuntu-latest
# needs: build-mails
test-back:
runs-on: ubuntu-latest
needs: build-mails
# defaults:
# run:
# working-directory: src/backend
defaults:
run:
working-directory: src/backend
# services:
# postgres:
# image: postgres:16
# env:
# POSTGRES_DB: impress
# POSTGRES_USER: dinum
# POSTGRES_PASSWORD: pass
# ports:
# - 5432:5432
# # needed because the postgres container does not provide a healthcheck
# options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
services:
postgres:
image: postgres:16
env:
POSTGRES_DB: impress
POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass
ports:
- 5432:5432
# needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
# env:
# DJANGO_CONFIGURATION: Test
# DJANGO_SETTINGS_MODULE: impress.settings
# DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
# OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
# DB_HOST: localhost
# DB_NAME: impress
# DB_USER: dinum
# DB_PASSWORD: pass
# DB_PORT: 5432
# STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
# AWS_S3_ENDPOINT_URL: http://localhost:9000
# AWS_S3_ACCESS_KEY_ID: impress
# AWS_S3_SECRET_ACCESS_KEY: password
env:
DJANGO_CONFIGURATION: Test
DJANGO_SETTINGS_MODULE: impress.settings
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
DB_HOST: localhost
DB_NAME: impress
DB_USER: dinum
DB_PASSWORD: pass
DB_PORT: 5432
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_ACCESS_KEY_ID: impress
AWS_S3_SECRET_ACCESS_KEY: password
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
steps:
- name: Checkout repository
uses: actions/checkout@v4
# - name: Create writable /data
# run: |
# sudo mkdir -p /data/media && \
# sudo mkdir -p /data/static
- name: Create writable /data
run: |
sudo mkdir -p /data/media && \
sudo mkdir -p /data/static
# - name: Restore the mail templates
# uses: actions/cache@v4
# id: mail-templates
# with:
# path: "src/backend/core/templates/mail"
# key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Restore the mail templates
uses: actions/cache@v4
id: mail-templates
with:
path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }}
# - name: Start Minio
# run: |
# docker pull minio/minio
# docker run -d --name minio \
# -p 9000:9000 \
# -e "MINIO_ACCESS_KEY=impress" \
# -e "MINIO_SECRET_KEY=password" \
# -v /data/media:/data \
# minio/minio server --console-address :9001 /data
- name: Start Minio
run: |
docker pull minio/minio
docker run -d --name minio \
-p 9000:9000 \
-e "MINIO_ACCESS_KEY=impress" \
-e "MINIO_SECRET_KEY=password" \
-v /data/media:/data \
minio/minio server --console-address :9001 /data
# - name: Configure MinIO
# run: |
# MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
# docker exec ${MINIO} sh -c \
# "mc alias set impress http://localhost:9000 impress password && \
# mc alias ls && \
# mc mb impress/impress-media-storage && \
# mc version enable impress/impress-media-storage"
- name: Configure MinIO
run: |
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
docker exec ${MINIO} sh -c \
"mc alias set impress http://localhost:9000 impress password && \
mc alias ls && \
mc mb impress/impress-media-storage && \
mc version enable impress/impress-media-storage"
# - name: Install Python
# uses: actions/setup-python@v3
# with:
# python-version: "3.10"
- name: Install Python
uses: actions/setup-python@v3
with:
python-version: "3.10"
# - name: Install development dependencies
# run: pip install --user .[dev]
- name: Install development dependencies
run: pip install --user .[dev]
# - name: Install gettext (required to compile messages)
# run: |
# sudo apt-get update
# sudo apt-get install -y gettext pandoc
- name: Install gettext (required to compile messages)
run: |
sudo apt-get update
sudo apt-get install -y gettext pandoc
# - name: Generate a MO file from strings extracted from the project
# run: python manage.py compilemessages
- name: Generate a MO file from strings extracted from the project
run: python manage.py compilemessages
# - name: Run tests
# run: ~/.local/bin/pytest -n 2
- name: Run tests
run: ~/.local/bin/pytest -n 2
+2 -9
View File
@@ -11,21 +11,13 @@ and this project adheres to
## Added
- ✨Add link public/authenticated/restricted access with read/editor roles #234
- ✨(frontend) add copy link button #235
- 🛂(frontend) access public docs without being logged #235
## Changed
- ♻️ Allow null titles on documents for easier creation #234
- 🛂(backend) stop to list public doc to everyone #234
- 🚚(frontend) change visibility in share modal #235
## Fixed
- 🐛 Fix forcing ID when creating a document via API endpoint #234
- 🐛 Rebuild frontend dev container from makefile #248
## [1.3.0] - 2024-09-05
@@ -40,6 +32,7 @@ and this project adheres to
- 💄(frontend) code background darkened on editor #214
- 🔥(frontend) hide markdown button if not text #213
- 🛂(backend) stop to list public doc to everyone #234
## Fixed
@@ -155,4 +148,4 @@ and this project adheres to
[1.2.0]: https://github.com/numerique-gouv/impress/releases/v1.2.0
[1.1.0]: https://github.com/numerique-gouv/impress/releases/v1.1.0
[1.0.0]: https://github.com/numerique-gouv/impress/releases/v1.0.0
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
[0.1.0]: https://github.com/numerique-gouv/impress/releases/v0.1.0
-1
View File
@@ -92,7 +92,6 @@ bootstrap: \
# -- Docker/compose
build: ## build the app-dev container
@$(COMPOSE) build app-dev --no-cache
@$(COMPOSE) build frontend-dev --no-cache
.PHONY: build
down: ## stop and remove containers, networks, images, and volumes
+2 -3
View File
@@ -16,8 +16,8 @@ class UserSerializer(serializers.ModelSerializer):
class Meta:
model = models.User
fields = ["id", "sub", "email"]
read_only_fields = ["id", "sub", "email"]
fields = ["id", "email"]
read_only_fields = ["id", "email"]
class BaseAccessSerializer(serializers.ModelSerializer):
@@ -148,7 +148,6 @@ class DocumentSerializer(BaseResourceSerializer):
"title",
"accesses",
"abilities",
"is_e2ee",
"link_role",
"link_reach",
"created_at",
+4 -4
View File
@@ -7,8 +7,8 @@ from urllib.parse import urlparse
from django.conf import settings
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.exceptions import ValidationError
from django.core.files.storage import default_storage
from django.db import IntegrityError
from django.db.models import (
OuterRef,
Q,
@@ -358,11 +358,11 @@ class DocumentViewSet(
try:
# Add a trace that the user visited the document (this is needed to include
# the document in the user's list view)
models.LinkTrace.objects.create(
models.LinkTrace.objects.update_or_create(
document=instance,
user=self.request.user,
)
except ValidationError:
except IntegrityError:
# The trace already exists, so we just pass without doing anything
pass
@@ -375,7 +375,7 @@ class DocumentViewSet(
to the document
"""
if not request.user.is_authenticated:
raise exceptions.PermissionDenied("Authentication required.")
return drf_response.Response([])
document = self.get_object()
user = request.user
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='document',
name='link_reach',
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='authenticated', max_length=20),
field=models.CharField(choices=[('restricted', 'Restricted'), ('authenticated', 'Authenticated'), ('public', 'Public')], default='restricted', max_length=20),
),
migrations.AddField(
model_name='document',
@@ -1,18 +0,0 @@
# Generated by Django 5.1 on 2024-09-12 13:53
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0005_remove_document_is_public_alter_document_link_reach_and_more'),
]
operations = [
migrations.AddField(
model_name='document',
name='is_e2ee',
field=models.BooleanField(default=False),
),
]
+1 -2
View File
@@ -324,12 +324,11 @@ class Document(BaseModel):
link_reach = models.CharField(
max_length=20,
choices=LinkReachChoices.choices,
default=LinkReachChoices.AUTHENTICATED,
default=LinkReachChoices.RESTRICTED,
)
link_role = models.CharField(
max_length=20, choices=LinkRoleChoices.choices, default=LinkRoleChoices.READER
)
is_e2ee = models.BooleanField(default=False)
_content = None
@@ -29,8 +29,8 @@ def test_api_document_versions_list_anonymous(role, reach):
response = APIClient().get(f"/api/v1.0/documents/{document.id!s}/versions/")
assert response.status_code == 403
assert response.json() == {"detail": "Authentication required."}
assert response.status_code == 200
assert response.json() == []
@pytest.mark.parametrize("reach", models.LinkReachChoices.values)
@@ -5,7 +5,7 @@ Tests for Documents API endpoint in impress's core app: retrieve
import pytest
from rest_framework.test import APIClient
from core import factories, models
from core import factories
from core.api import serializers
pytestmark = pytest.mark.django_db
@@ -33,7 +33,6 @@ def test_api_documents_retrieve_anonymous_public():
"versions_retrieve": False,
},
"accesses": [],
"is_e2ee": False,
"link_reach": "public",
"link_role": document.link_role,
"title": document.title,
@@ -88,7 +87,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"versions_retrieve": False,
},
"accesses": [],
"is_e2ee": False,
"link_reach": reach,
"link_role": document.link_role,
"title": document.title,
@@ -96,38 +94,6 @@ def test_api_documents_retrieve_authenticated_unrelated_public_or_authenticated(
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
"updated_at": document.updated_at.isoformat().replace("+00:00", "Z"),
}
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
)
@pytest.mark.parametrize("reach", ["public", "authenticated"])
def test_api_documents_retrieve_authenticated_trace_twice(reach):
"""
Accessing a document several times should not raise any error even though the
trace already exists for this document and user.
"""
user = factories.UserFactory()
client = APIClient()
client.force_login(user)
document = factories.DocumentFactory(link_reach=reach)
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is False
)
client.get(
f"/api/v1.0/documents/{document.id!s}/",
)
assert (
models.LinkTrace.objects.filter(document=document, user=user).exists() is True
)
# A second visit should not raise any error
response = client.get(f"/api/v1.0/documents/{document.id!s}/")
assert response.status_code == 200
def test_api_documents_retrieve_authenticated_unrelated_restricted():
@@ -196,7 +162,6 @@ def test_api_documents_retrieve_authenticated_related_direct():
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_e2ee": False,
"link_reach": document.link_reach,
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
@@ -335,7 +300,6 @@ def test_api_documents_retrieve_authenticated_related_team_members(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_e2ee": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
@@ -456,7 +420,6 @@ def test_api_documents_retrieve_authenticated_related_team_administrators(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_e2ee": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
@@ -581,7 +544,6 @@ def test_api_documents_retrieve_authenticated_related_team_owners(
"title": document.title,
"content": document.content,
"abilities": document.get_abilities(user),
"is_e2ee": False,
"link_reach": "restricted",
"link_role": document.link_role,
"created_at": document.created_at.isoformat().replace("+00:00", "Z"),
+1 -1
View File
@@ -12,7 +12,7 @@
"test:ui::chromium": "yarn test:ui --project=chromium"
},
"devDependencies": {
"@playwright/test": "1.47.0",
"@playwright/test": "1.46.1",
"@types/node": "*",
"@types/pdf-parse": "1.1.4",
"eslint-config-impress": "*",
+1 -1
View File
@@ -2,4 +2,4 @@
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information.
// see https://nextjs.org/docs/basic-features/typescript for more information.
+4 -9
View File
@@ -21,28 +21,24 @@
"@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.13.5",
"@openfun/cunningham-react": "2.9.4",
"@socialgouv/e2esdk-client": "1.0.0-beta.28",
"@socialgouv/e2esdk-devtools": "1.0.0-beta.38",
"@socialgouv/e2esdk-react": "1.0.0-beta.28",
"@tanstack/react-query": "5.55.4",
"i18next": "23.15.1",
"@tanstack/react-query": "5.53.2",
"i18next": "23.14.0",
"idb": "8.0.0",
"lodash": "4.17.21",
"luxon": "3.5.0",
"next": "14.2.9",
"next": "14.2.7",
"react": "*",
"react-aria-components": "1.3.3",
"react-dom": "*",
"react-i18next": "15.0.1",
"react-select": "5.8.0",
"styled-components": "6.1.13",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "4.5.5"
},
"devDependencies": {
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.55.4",
"@tanstack/react-query-devtools": "5.53.2",
"@testing-library/dom": "10.4.0",
"@testing-library/jest-dom": "6.5.0",
"@testing-library/react": "16.0.1",
@@ -65,7 +61,6 @@
"stylelint-config-standard": "36.0.1",
"stylelint-prettier": "5.0.2",
"typescript": "*",
"webpack": "5.94.0",
"workbox-webpack-plugin": "7.1.0"
}
}
@@ -1,7 +1,5 @@
import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { E2ESDKClientProvider } from '@socialgouv/e2esdk-react';
import { e2esdkClient } from './auth/useAuthStore';
import { useCunninghamTheme } from '@/cunningham';
import '@/i18n/initI18n';
@@ -29,9 +27,7 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return (
<QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}>
<E2ESDKClientProvider client={e2esdkClient}>
<Auth>{children}</Auth>
</E2ESDKClientProvider>
<Auth>{children}</Auth>
</CunninghamProvider>
</QueryClientProvider>
);
@@ -2,12 +2,10 @@
* Represents user retrieved from the API.
* @interface User
* @property {string} id - The id of the user.
* @property {string} sub - The `sub` field of OIDC
* @property {string} email - The email of the user.
* @property {string} name - The name of the user.
*/
export interface User {
id: string;
sub: string;
email: string;
}
@@ -5,31 +5,18 @@ import { baseApiUrl } from '@/core/conf';
import { User, getMe } from './api';
import { PATH_AUTH_LOCAL_STORAGE } from './conf';
import { Client, PublicUserIdentity } from '@socialgouv/e2esdk-client';
import { identity } from 'lodash';
export const e2esdkClient = new Client({
// Point it to where your server is listening
serverURL: 'https://app-a5a1b445-32e0-4cf4-a478-821a48f86ccf.cleverapps.io',
// Pass the signature public key you configured for the server
serverSignaturePublicKey: 'ayfva9SUh0mfgmifUtxcdLp4HriHJiqefEKnvYgY4qM',
});
interface AuthStore {
initiated: boolean;
authenticated: boolean;
readyForEncryption: boolean;
initAuth: () => void;
logout: () => void;
login: () => void;
endToEndData?: PublicUserIdentity;
userData?: User;
}
const initialState = {
initiated: false,
authenticated: false,
readyForEncryption: false,
userData: undefined,
};
@@ -37,51 +24,22 @@ export const useAuthStore = create<AuthStore>((set) => ({
initiated: initialState.initiated,
authenticated: initialState.authenticated,
userData: initialState.userData,
readyForEncryption: initialState.readyForEncryption,
initAuth: () => {
getMe()
.then(
(data: User) => {
// If a path is stored in the local storage, we redirect to it
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
window.location.replace(path_auth);
return;
}
.then((data: User) => {
// If a path is stored in the local storage, we redirect to it
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) {
localStorage.removeItem(PATH_AUTH_LOCAL_STORAGE);
window.location.replace(path_auth);
return;
}
set({ authenticated: true, userData: data });
return e2esdkClient
.signup(data.sub)
.then(() => data)
.catch(() => data);
},
() => {},
)
.then(
(data) => {
set({ readyForEncryption: true });
if (data) {
return e2esdkClient.login(data.sub);
}
},
(e) => {
throw e;
//if (data) {
// return e2esdkClient.login(data.sub);
//}
//fail
},
)
.then((publicIdentity: PublicUserIdentity | null | undefined) => {
if (!publicIdentity) throw Error('exploding');
console.log('publicIdentity', publicIdentity);
set({ endToEndData: publicIdentity });
set({ authenticated: true, userData: data });
})
.catch(() => {})
.finally(() => {
console.log('finally');
set({ initiated: true });
});
},
@@ -1,4 +1,4 @@
import { Block, BlockNoteEditor as BlockNoteEditorCore, PartialBlock } from '@blocknote/core';
import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css';
@@ -17,7 +17,6 @@ import { useDocStore } from '../stores';
import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar';
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
const cssEditor = `
&, & > .bn-container, & .ProseMirror {
@@ -72,8 +71,7 @@ export const BlockNoteContent = ({
const { userData } = useAuthStore();
const { setStore, docsStore } = useDocStore();
const canSave = doc.abilities.partial_update && !isVersion;
const e2eClient = useE2ESDKClient();
useSaveDoc(doc.id, provider.document, canSave);
const storedEditor = docsStore?.[storeId]?.editor;
const {
mutateAsync: createDocAttachment,
@@ -101,46 +99,18 @@ export const BlockNoteContent = ({
return storedEditor;
}
// TODO decrypt doc.content
//localStorage.getItem('KEY');
const docId = doc.id;
const purpose = `docs:${docId}`;
const key = e2eClient.findKeyByPurpose(purpose);
console.log('purpose', purpose, 'key', key);
let plaintextContent: Array<PartialBlock> | undefined;
if (!key) {
alert('probleme de key');
return;
} else {
if (doc.content) {
plaintextContent = JSON.parse(e2eClient.decrypt(
doc.content,
key.keychainFingerprint,
) as string) as Array<PartialBlock>;
console.log('decryptedMessage', plaintextContent);
} else {
plaintextContent = undefined;
}
}
return BlockNoteEditorCore.create({
// collaboration: {
// provider,
// fragment: provider.document.getXmlFragment('document-store'),
// user: {
// name: userData?.email || 'Anonymous',
// color: randomColor(),
// },
// },
collaboration: {
provider,
fragment: provider.document.getXmlFragment('document-store'),
user: {
name: userData?.email || 'Anonymous',
color: randomColor(),
},
},
uploadFile,
initialContent: plaintextContent,
});
}, [doc.content, storedEditor, uploadFile]);
console.log("useSaveDoc", doc.id, provider.document, canSave, editor);
useSaveDoc(doc.id, provider.document, canSave, editor);
}, [provider, storedEditor, uploadFile, userData?.email]);
useEffect(() => {
setStore(storeId, { editor });
@@ -1,4 +1,3 @@
import { BlockNoteEditor } from '@blocknote/core';
import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as Y from 'yjs';
@@ -7,24 +6,17 @@ import { useUpdateDoc } from '@/features/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
import { toBase64 } from '../utils';
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
const useSaveDoc = (
docId: string,
doc: Y.Doc,
canSave: boolean,
editor: BlockNoteEditor,
) => {
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
});
const e2eClient = useE2ESDKClient();
const [initialDoc, setInitialDoc] = useState<string>(
JSON.stringify(editor.document)
toBase64(Y.encodeStateAsUpdate(doc)),
);
useEffect(() => {
setInitialDoc(JSON.stringify(editor.document));
setInitialDoc(toBase64(Y.encodeStateAsUpdate(doc)));
}, [doc]);
/**
@@ -40,7 +32,7 @@ const useSaveDoc = (
transaction: Y.Transaction,
) => {
if (!transaction.local) {
setInitialDoc(JSON.stringify(editor.document));
setInitialDoc(toBase64(Y.encodeStateAsUpdate(updatedDoc)));
}
};
@@ -55,41 +47,23 @@ const useSaveDoc = (
* Check if the doc has been updated and can be saved.
*/
const hasChanged = useCallback(() => {
return initialDoc !== JSON.stringify(editor.document);
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
return initialDoc !== newDoc;
}, [doc, initialDoc]);
const shouldSave = useCallback(() => {
console.log('hasChanged', hasChanged(), 'canSave', canSave);
return hasChanged() && canSave;
}, [canSave, hasChanged]);
const saveDoc = useCallback(() => {
const newDoc = JSON.stringify(editor.document);
//const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
// TODO encode the content
const purpose = `docs:${docId}`;
const key = e2eClient.findKeyByPurpose(purpose);
console.log("purpose", purpose, "key", key);
if (!key) {
alert('probleme de key');
return;
}
const encrypted = e2eClient.encrypt(newDoc, key.keychainFingerprint);
console.log('encrypted', encrypted);
// todo
const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
setInitialDoc(newDoc);
updateDoc({
id: docId,
content: encrypted,
content: newDoc,
});
}, [docId, editor?.document, updateDoc]);
}, [doc, docId, updateDoc]);
const timeout = useRef<NodeJS.Timeout>();
const router = useRouter();
@@ -100,12 +74,10 @@ const useSaveDoc = (
}
const onSave = (e?: Event) => {
console.log('entered onSave');
if (!shouldSave()) {
return;
}
console.log('will save');
saveDoc();
/**
@@ -124,7 +96,7 @@ const useSaveDoc = (
};
// Save every minute
timeout.current = setInterval(onSave, 1000);
timeout.current = setInterval(onSave, 60000);
// Save when the user leaves the page
addEventListener('beforeunload', onSave);
// Save when the user navigates to another page
@@ -26,9 +26,9 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
guid: storeId,
});
// if (initialDoc) {
// Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
// }
if (initialDoc) {
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
}
const provider = new HocuspocusProvider({
url: providerUrl(storeId),
@@ -1,5 +1,4 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { e2esdkClient } from '@/core/auth/useAuthStore';
import { APIError, errorCauses, fetchAPI } from '@/api';
@@ -14,7 +13,6 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
method: 'POST',
body: JSON.stringify({
title,
is_e2ee: true
}),
});
@@ -22,16 +20,7 @@ export const createDoc = async ({ title }: CreateDocParam): Promise<Doc> => {
throw new APIError('Failed to create the doc', await errorCauses(response));
}
const resp = await (response.json() as Promise<Doc>);
const { keychainFingerprint } = await e2esdkClient.createNewKeychain(
`docs:${resp.id}`,
'secretBox'
);
console.log('new e2ee keychain registered', keychainFingerprint);
return resp;
return response.json() as Promise<Doc>;
};
interface CreateDocProps {
@@ -37,7 +37,6 @@ export interface Doc {
accesses: Access[];
created_at: string;
updated_at: string;
is_e2ee: boolean;
abilities: {
destroy: boolean;
link_configuration: boolean;
+6 -9
View File
@@ -25,18 +25,15 @@
"i18n:test": "yarn I18N run test"
},
"resolutions": {
"@blocknote/core": "0.15.7",
"@blocknote/mantine": "0.15.7",
"@blocknote/react": "0.15.7",
"@types/node": "20.16.5",
"@blocknote/core": "0.15.6",
"@blocknote/mantine": "0.15.6",
"@blocknote/react": "0.15.6",
"@types/node": "20.16.3",
"@types/react-dom": "18.3.0",
"@typescript-eslint/eslint-plugin": "8.5.0",
"@typescript-eslint/parser": "8.5.0",
"cross-env": "7.0.3",
"eslint": "8.57.0",
"react": "18.3.1",
"react-dom": "18.3.1",
"typescript": "5.6.2",
"yjs": "13.6.19"
"typescript": "5.5.4",
"yjs": "13.6.18"
}
}
@@ -6,20 +6,19 @@
"lint": "eslint --ext .js ."
},
"dependencies": {
"@next/eslint-plugin-next": "14.2.9",
"@next/eslint-plugin-next": "14.2.7",
"@tanstack/eslint-plugin-query": "5.53.0",
"@typescript-eslint/eslint-plugin": "*",
"@typescript-eslint/parser": "*",
"eslint": "*",
"eslint-config-next": "14.2.9",
"@typescript-eslint/eslint-plugin": "8.3.0",
"@typescript-eslint/parser": "8.3.0",
"eslint": "8.57.0",
"eslint-config-next": "14.2.7",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-jest": "28.8.3",
"eslint-plugin-jsx-a11y": "6.10.0",
"eslint-plugin-import": "2.29.1",
"eslint-plugin-jest": "28.8.2",
"eslint-plugin-jsx-a11y": "6.9.0",
"eslint-plugin-playwright": "1.6.2",
"eslint-plugin-prettier": "5.2.1",
"eslint-plugin-react": "7.35.2",
"eslint-plugin-testing-library": "6.3.0",
"prettier": "3.3.3"
"eslint-plugin-react": "7.35.0",
"eslint-plugin-testing-library": "6.3.0"
}
}
+1 -1
View File
@@ -14,7 +14,7 @@
"@types/jest": "29.5.12",
"@types/node": "*",
"eslint-config-impress": "*",
"eslint-plugin-import": "2.30.0",
"eslint-plugin-import": "2.29.1",
"i18next-parser": "9.0.2",
"jest": "29.7.0",
"ts-jest": "29.2.5",
+1 -2
View File
@@ -15,8 +15,7 @@
"node": ">=18"
},
"dependencies": {
"@hocuspocus/server": "2.13.5",
"y-protocols": "1.0.6"
"@hocuspocus/server": "2.13.5"
},
"devDependencies": {
"@types/node": "*",
+11907 -13180
View File
File diff suppressed because it is too large Load Diff