Compare commits

...

7 Commits

Author SHA1 Message Date
Raito Bezarius
7a99913d0e feat(provider): add E2EESDKClientProvider wrapping the app
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-09-12 16:40:53 +02:00
Julien Bouquillon
980a18aff2 fix-ts 2024-09-12 16:31:03 +02:00
Julien Bouquillon
6f655a20ac wip 2024-09-12 16:26:04 +02:00
Julien Bouquillon
39d93e330e wip 2024-09-12 16:13:08 +02:00
Raito Bezarius
cc2d034abf wip! wip! wip! auth for E2EE
Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-09-12 15:51:12 +02:00
Raito Bezarius
312a92a8e0 feat(api,front): expose the sub OIDC field for the frontend
Useful for E2EE encryption identification.

Signed-off-by: Raito Bezarius <masterancpp@gmail.com>
2024-09-12 15:51:12 +02:00
Anthony LC
7a6c0f0eee ♻️(frontend) save editor as json
Stop to save a y.js doc to save the json editor.
2024-09-12 15:20:55 +02:00
10 changed files with 13449 additions and 12130 deletions

View File

@@ -9,52 +9,52 @@ on:
- "*" - "*"
jobs: jobs:
lint-git: # lint-git:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # Makes sense only for pull requests # if: github.event_name == 'pull_request' # Makes sense only for pull requests
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v2 # uses: actions/checkout@v2
with: # with:
fetch-depth: 0 # fetch-depth: 0
- name: show # - name: show
run: git log # run: git log
- name: Enforce absence of print statements in code # - name: Enforce absence of print statements in code
run: | # run: |
! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print(" # ! git diff origin/${{ github.event.pull_request.base.ref }}..HEAD -- . ':(exclude)**/impress.yml' | grep "print("
- name: Check absence of fixup commits # - name: Check absence of fixup commits
run: | # run: |
! git log | grep 'fixup!' # ! git log | grep 'fixup!'
- name: Install gitlint # - name: Install gitlint
run: pip install --user requests gitlint # run: pip install --user requests gitlint
- name: Lint commit messages added to main # - name: Lint commit messages added to main
run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD # run: ~/.local/bin/gitlint --commits origin/${{ github.event.pull_request.base.ref }}..HEAD
check-changelog: # check-changelog:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
if: | # if: |
contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false && # contains(github.event.pull_request.labels.*.name, 'noChangeLog') == false &&
github.event_name == 'pull_request' # github.event_name == 'pull_request'
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v3 # uses: actions/checkout@v3
with: # with:
fetch-depth: 50 # fetch-depth: 50
- name: Check that the CHANGELOG has been modified in the current branch # - 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' # run: git diff --name-only ${{ github.event.pull_request.base.sha }} ${{ github.event.after }} | grep 'CHANGELOG.md'
lint-changelog: # lint-changelog:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v2 # uses: actions/checkout@v2
- name: Check CHANGELOG max line length # - name: Check CHANGELOG max line length
run: | # run: |
max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L) # max_line_length=$(cat CHANGELOG.md | grep -Ev "^\[.*\]: https://github.com" | wc -L)
if [ $max_line_length -ge 80 ]; then # if [ $max_line_length -ge 80 ]; then
echo "ERROR: CHANGELOG has lines longer than 80 characters." # echo "ERROR: CHANGELOG has lines longer than 80 characters."
exit 1 # exit 1
fi # fi
build-mails: build-mails:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -96,112 +96,112 @@ jobs:
path: "src/backend/core/templates/mail" path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }} key: mail-templates-${{ hashFiles('src/mail/mjml') }}
lint-back: # lint-back:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
defaults: # defaults:
run: # run:
working-directory: src/backend # working-directory: src/backend
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v2 # uses: actions/checkout@v2
- name: Install Python # - name: Install Python
uses: actions/setup-python@v3 # uses: actions/setup-python@v3
with: # with:
python-version: "3.10" # python-version: "3.10"
- name: Install development dependencies # - name: Install development dependencies
run: pip install --user .[dev] # run: pip install --user .[dev]
- name: Check code formatting with ruff # - name: Check code formatting with ruff
run: ~/.local/bin/ruff format . --diff # run: ~/.local/bin/ruff format . --diff
- name: Lint code with ruff # - name: Lint code with ruff
run: ~/.local/bin/ruff check . # run: ~/.local/bin/ruff check .
- name: Lint code with pylint # - name: Lint code with pylint
run: ~/.local/bin/pylint . # run: ~/.local/bin/pylint .
test-back: # test-back:
runs-on: ubuntu-latest # runs-on: ubuntu-latest
needs: build-mails # needs: build-mails
defaults: # defaults:
run: # run:
working-directory: src/backend # working-directory: src/backend
services: # services:
postgres: # postgres:
image: postgres:16 # image: postgres:16
env: # env:
POSTGRES_DB: impress # POSTGRES_DB: impress
POSTGRES_USER: dinum # POSTGRES_USER: dinum
POSTGRES_PASSWORD: pass # POSTGRES_PASSWORD: pass
ports: # ports:
- 5432:5432 # - 5432:5432
# needed because the postgres container does not provide a healthcheck # # needed because the postgres container does not provide a healthcheck
options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 # options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5
env: # env:
DJANGO_CONFIGURATION: Test # DJANGO_CONFIGURATION: Test
DJANGO_SETTINGS_MODULE: impress.settings # DJANGO_SETTINGS_MODULE: impress.settings
DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly # DJANGO_SECRET_KEY: ThisIsAnExampleKeyForTestPurposeOnly
OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only # OIDC_OP_JWKS_ENDPOINT: /endpoint-for-test-purpose-only
DB_HOST: localhost # DB_HOST: localhost
DB_NAME: impress # DB_NAME: impress
DB_USER: dinum # DB_USER: dinum
DB_PASSWORD: pass # DB_PASSWORD: pass
DB_PORT: 5432 # DB_PORT: 5432
STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage # STORAGES_STATICFILES_BACKEND: django.contrib.staticfiles.storage.StaticFilesStorage
AWS_S3_ENDPOINT_URL: http://localhost:9000 # AWS_S3_ENDPOINT_URL: http://localhost:9000
AWS_S3_ACCESS_KEY_ID: impress # AWS_S3_ACCESS_KEY_ID: impress
AWS_S3_SECRET_ACCESS_KEY: password # AWS_S3_SECRET_ACCESS_KEY: password
steps: # steps:
- name: Checkout repository # - name: Checkout repository
uses: actions/checkout@v4 # uses: actions/checkout@v4
- name: Create writable /data # - name: Create writable /data
run: | # run: |
sudo mkdir -p /data/media && \ # sudo mkdir -p /data/media && \
sudo mkdir -p /data/static # sudo mkdir -p /data/static
- name: Restore the mail templates # - name: Restore the mail templates
uses: actions/cache@v4 # uses: actions/cache@v4
id: mail-templates # id: mail-templates
with: # with:
path: "src/backend/core/templates/mail" # path: "src/backend/core/templates/mail"
key: mail-templates-${{ hashFiles('src/mail/mjml') }} # key: mail-templates-${{ hashFiles('src/mail/mjml') }}
- name: Start Minio # - name: Start Minio
run: | # run: |
docker pull minio/minio # docker pull minio/minio
docker run -d --name minio \ # docker run -d --name minio \
-p 9000:9000 \ # -p 9000:9000 \
-e "MINIO_ACCESS_KEY=impress" \ # -e "MINIO_ACCESS_KEY=impress" \
-e "MINIO_SECRET_KEY=password" \ # -e "MINIO_SECRET_KEY=password" \
-v /data/media:/data \ # -v /data/media:/data \
minio/minio server --console-address :9001 /data # minio/minio server --console-address :9001 /data
- name: Configure MinIO # - name: Configure MinIO
run: | # run: |
MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/') # MINIO=$(docker ps | grep minio/minio | sed -E 's/.*\s+([a-zA-Z0-9_-]+)$/\1/')
docker exec ${MINIO} sh -c \ # docker exec ${MINIO} sh -c \
"mc alias set impress http://localhost:9000 impress password && \ # "mc alias set impress http://localhost:9000 impress password && \
mc alias ls && \ # mc alias ls && \
mc mb impress/impress-media-storage && \ # mc mb impress/impress-media-storage && \
mc version enable impress/impress-media-storage" # mc version enable impress/impress-media-storage"
- name: Install Python # - name: Install Python
uses: actions/setup-python@v3 # uses: actions/setup-python@v3
with: # with:
python-version: "3.10" # python-version: "3.10"
- name: Install development dependencies # - name: Install development dependencies
run: pip install --user .[dev] # run: pip install --user .[dev]
- name: Install gettext (required to compile messages) # - name: Install gettext (required to compile messages)
run: | # run: |
sudo apt-get update # sudo apt-get update
sudo apt-get install -y gettext pandoc # sudo apt-get install -y gettext pandoc
- name: Generate a MO file from strings extracted from the project # - name: Generate a MO file from strings extracted from the project
run: python manage.py compilemessages # run: python manage.py compilemessages
- name: Run tests # - name: Run tests
run: ~/.local/bin/pytest -n 2 # run: ~/.local/bin/pytest -n 2

View File

@@ -16,8 +16,8 @@ class UserSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.User model = models.User
fields = ["id", "email"] fields = ["id", "sub", "email"]
read_only_fields = ["id", "email"] read_only_fields = ["id", "sub", "email"]
class BaseAccessSerializer(serializers.ModelSerializer): class BaseAccessSerializer(serializers.ModelSerializer):

View File

@@ -21,6 +21,9 @@
"@gouvfr-lasuite/integration": "1.0.2", "@gouvfr-lasuite/integration": "1.0.2",
"@hocuspocus/provider": "2.13.5", "@hocuspocus/provider": "2.13.5",
"@openfun/cunningham-react": "2.9.4", "@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", "@tanstack/react-query": "5.55.4",
"i18next": "23.15.1", "i18next": "23.15.1",
"idb": "8.0.0", "idb": "8.0.0",
@@ -33,8 +36,8 @@
"react-i18next": "15.0.1", "react-i18next": "15.0.1",
"react-select": "5.8.0", "react-select": "5.8.0",
"styled-components": "6.1.13", "styled-components": "6.1.13",
"yjs": "*",
"y-protocols": "1.0.6", "y-protocols": "1.0.6",
"yjs": "*",
"zustand": "4.5.5" "zustand": "4.5.5"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,5 +1,7 @@
import { CunninghamProvider } from '@openfun/cunningham-react'; import { CunninghamProvider } from '@openfun/cunningham-react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { E2ESDKClientProvider } from '@socialgouv/e2esdk-react';
import { e2esdkClient } from './auth/useAuthStore';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import '@/i18n/initI18n'; import '@/i18n/initI18n';
@@ -27,7 +29,9 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
<CunninghamProvider theme={theme}> <CunninghamProvider theme={theme}>
<E2ESDKClientProvider client={e2esdkClient}>
<Auth>{children}</Auth> <Auth>{children}</Auth>
</E2ESDKClientProvider>
</CunninghamProvider> </CunninghamProvider>
</QueryClientProvider> </QueryClientProvider>
); );

View File

@@ -2,10 +2,12 @@
* Represents user retrieved from the API. * Represents user retrieved from the API.
* @interface User * @interface User
* @property {string} id - The id of the 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} email - The email of the user.
* @property {string} name - The name of the user. * @property {string} name - The name of the user.
*/ */
export interface User { export interface User {
id: string; id: string;
sub: string;
email: string; email: string;
} }

View File

@@ -5,18 +5,31 @@ import { baseApiUrl } from '@/core/conf';
import { User, getMe } from './api'; import { User, getMe } from './api';
import { PATH_AUTH_LOCAL_STORAGE } from './conf'; 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 { interface AuthStore {
initiated: boolean; initiated: boolean;
authenticated: boolean; authenticated: boolean;
readyForEncryption: boolean;
initAuth: () => void; initAuth: () => void;
logout: () => void; logout: () => void;
login: () => void; login: () => void;
endToEndData?: PublicUserIdentity;
userData?: User; userData?: User;
} }
const initialState = { const initialState = {
initiated: false, initiated: false,
authenticated: false, authenticated: false,
readyForEncryption: false,
userData: undefined, userData: undefined,
}; };
@@ -24,10 +37,12 @@ export const useAuthStore = create<AuthStore>((set) => ({
initiated: initialState.initiated, initiated: initialState.initiated,
authenticated: initialState.authenticated, authenticated: initialState.authenticated,
userData: initialState.userData, userData: initialState.userData,
readyForEncryption: initialState.readyForEncryption,
initAuth: () => { initAuth: () => {
getMe() getMe()
.then((data: User) => { .then(
(data: User) => {
// If a path is stored in the local storage, we redirect to it // If a path is stored in the local storage, we redirect to it
const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE); const path_auth = localStorage.getItem(PATH_AUTH_LOCAL_STORAGE);
if (path_auth) { if (path_auth) {
@@ -37,9 +52,36 @@ export const useAuthStore = create<AuthStore>((set) => ({
} }
set({ authenticated: true, userData: data }); 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 });
}) })
.catch(() => {}) .catch(() => {})
.finally(() => { .finally(() => {
console.log('finally');
set({ initiated: true }); set({ initiated: true });
}); });
}, },

View File

@@ -1,4 +1,4 @@
import { BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core'; import { Block, BlockNoteEditor as BlockNoteEditorCore } from '@blocknote/core';
import '@blocknote/core/fonts/inter.css'; import '@blocknote/core/fonts/inter.css';
import { BlockNoteView } from '@blocknote/mantine'; import { BlockNoteView } from '@blocknote/mantine';
import '@blocknote/mantine/style.css'; import '@blocknote/mantine/style.css';
@@ -17,6 +17,7 @@ import { useDocStore } from '../stores';
import { randomColor } from '../utils'; import { randomColor } from '../utils';
import { BlockNoteToolbar } from './BlockNoteToolbar'; import { BlockNoteToolbar } from './BlockNoteToolbar';
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
const cssEditor = ` const cssEditor = `
&, & > .bn-container, & .ProseMirror { &, & > .bn-container, & .ProseMirror {
@@ -71,7 +72,8 @@ export const BlockNoteContent = ({
const { userData } = useAuthStore(); const { userData } = useAuthStore();
const { setStore, docsStore } = useDocStore(); const { setStore, docsStore } = useDocStore();
const canSave = doc.abilities.partial_update && !isVersion; const canSave = doc.abilities.partial_update && !isVersion;
useSaveDoc(doc.id, provider.document, canSave);
const e2eClient = useE2ESDKClient();
const storedEditor = docsStore?.[storeId]?.editor; const storedEditor = docsStore?.[storeId]?.editor;
const { const {
mutateAsync: createDocAttachment, mutateAsync: createDocAttachment,
@@ -99,18 +101,39 @@ export const BlockNoteContent = ({
return storedEditor; return storedEditor;
} }
// TODO decrypt doc.content
//localStorage.getItem('KEY');
const docId = 'uuid-du-doc';
const purpose = `doc:${docId}`;
const key = e2eClient.findKeyByPurpose(purpose);
if (!key) {
alert('probleme de key');
// return;
} else {
const decryptedMessage = e2eClient.decrypt(
doc.content,
key.keychainFingerprint,
);
console.log('decryptedMessage', decryptedMessage);
}
return BlockNoteEditorCore.create({ return BlockNoteEditorCore.create({
collaboration: { // collaboration: {
provider, // provider,
fragment: provider.document.getXmlFragment('document-store'), // fragment: provider.document.getXmlFragment('document-store'),
user: { // user: {
name: userData?.email || 'Anonymous', // name: userData?.email || 'Anonymous',
color: randomColor(), // color: randomColor(),
}, // },
}, // },
uploadFile, uploadFile,
initialContent: JSON.parse(doc.content),
}); });
}, [provider, storedEditor, uploadFile, userData?.email]); }, [doc.content, storedEditor, uploadFile]);
useSaveDoc(doc.id, provider.document, canSave, editor);
useEffect(() => { useEffect(() => {
setStore(storeId, { editor }); setStore(storeId, { editor });

View File

@@ -1,3 +1,4 @@
import { BlockNoteEditor } from '@blocknote/core';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import * as Y from 'yjs'; import * as Y from 'yjs';
@@ -6,11 +7,18 @@ import { useUpdateDoc } from '@/features/docs/doc-management/';
import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning'; import { KEY_LIST_DOC_VERSIONS } from '@/features/docs/doc-versioning';
import { toBase64 } from '../utils'; import { toBase64 } from '../utils';
import { useE2ESDKClient } from '@socialgouv/e2esdk-react';
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => { const useSaveDoc = (
docId: string,
doc: Y.Doc,
canSave: boolean,
editor: BlockNoteEditor,
) => {
const { mutate: updateDoc } = useUpdateDoc({ const { mutate: updateDoc } = useUpdateDoc({
listInvalideQueries: [KEY_LIST_DOC_VERSIONS], listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
}); });
const e2eClient = useE2ESDKClient();
const [initialDoc, setInitialDoc] = useState<string>( const [initialDoc, setInitialDoc] = useState<string>(
toBase64(Y.encodeStateAsUpdate(doc)), toBase64(Y.encodeStateAsUpdate(doc)),
); );
@@ -56,14 +64,32 @@ const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
}, [canSave, hasChanged]); }, [canSave, hasChanged]);
const saveDoc = useCallback(() => { const saveDoc = useCallback(() => {
const newDoc = toBase64(Y.encodeStateAsUpdate(doc)); const newDoc = JSON.stringify(editor.document);
//const newDoc = toBase64(Y.encodeStateAsUpdate(doc));
// TODO encode the content
const docId = 'uuid-du-doc';
const purpose = `doc:${docId}`;
const key = e2eClient.findKeyByPurpose(purpose);
if (!key) {
alert('probleme de key');
return;
}
const encrypted = e2eClient.encrypt(newDoc, key.keychainFingerprint);
console.log('encrypted', encrypted);
// todo
setInitialDoc(newDoc); setInitialDoc(newDoc);
updateDoc({ updateDoc({
id: docId, id: docId,
content: newDoc, content: newDoc,
}); });
}, [doc, docId, updateDoc]); }, [docId, editor?.document, updateDoc]);
const timeout = useRef<NodeJS.Timeout>(); const timeout = useRef<NodeJS.Timeout>();
const router = useRouter(); const router = useRouter();

View File

@@ -26,9 +26,9 @@ export const useDocStore = create<UseDocStore>((set, get) => ({
guid: storeId, guid: storeId,
}); });
if (initialDoc) { // if (initialDoc) {
Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64')); // Y.applyUpdate(doc, Buffer.from(initialDoc, 'base64'));
} // }
const provider = new HocuspocusProvider({ const provider = new HocuspocusProvider({
url: providerUrl(storeId), url: providerUrl(storeId),

File diff suppressed because it is too large Load Diff