Merge pull request #138 from ets-cfuhrman-pfe/109-tche-11--intgration-de-passportjs

109 tche 11  intgration de passportjs
This commit is contained in:
Gabriel Moisan Matte 2024-09-29 11:36:34 -04:00 committed by GitHub
commit 05381c7da9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1166 additions and 300 deletions

View file

@ -0,0 +1,83 @@
name: create-branch-images
on:
workflow_call:
workflow_dispatch:
push:
branches:
- 'main'
- 'dev'
tags:
- '**'
jobs:
docker:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: |
ghcr.io/${{ github.repository }}/frontend
ghcr.io/${{ github.repository }}/backend
ghcr.io/${{ github.repository }}/router
tags: |
type=schedule
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build and push frontend Docker image
uses: docker/build-push-action@v3
with:
context: ./client
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push backend Docker image
uses: docker/build-push-action@v3
with:
context: ./server
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push router Docker image
uses: docker/build-push-action@v3
with:
context: ./nginx
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

18
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,18 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug backend",
"skipFiles": [
"<node_internals>/**"
],
"program": "${workspaceFolder}/server/app.js",
"cwd":"${workspaceFolder}/server/"
}
]
}

370
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,6 @@
"nanoid": "^5.0.2", "nanoid": "^5.0.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-latex": "^2.0.0",
"react-modal": "^3.16.1", "react-modal": "^3.16.1",
"react-router-dom": "^6.26.2", "react-router-dom": "^6.26.2",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",

View file

@ -1,7 +1,7 @@
// TextType.test.ts // TextType.test.ts
import { TextFormat } from "gift-pegjs"; import { TextFormat } from "gift-pegjs";
import TextType from "../../../components/GiftTemplate/templates/TextType"; import textType from "../../../components/GiftTemplate/templates/TextType";
describe('TextType', () => { describe('TextType', () => {
it('should format text with basic characters correctly', () => { it('should format text with basic characters correctly', () => {
@ -10,7 +10,7 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Hello, world! 5 > 3, right?'; const expectedOutput = 'Hello, world! 5 > 3, right?';
expect(TextType({ text: input })).toBe(expectedOutput); expect(textType({ text: input })).toBe(expectedOutput);
}); });
it('should format text with newlines correctly', () => { it('should format text with newlines correctly', () => {
@ -19,7 +19,7 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Hello,<br>world!<br>5 > 3, right?'; const expectedOutput = 'Hello,<br>world!<br>5 > 3, right?';
expect(TextType({ text: input })).toBe(expectedOutput); expect(textType({ text: input })).toBe(expectedOutput);
}); });
it('should format text with LaTeX correctly', () => { it('should format text with LaTeX correctly', () => {
@ -33,7 +33,7 @@ describe('TextType', () => {
// by running the test and copying the "Received string:" in jest output // by running the test and copying the "Received string:" in jest output
// when it fails (assuming the output is correct) // when it fails (assuming the output is correct)
const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>'; const expectedOutput = '<span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(TextType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });
it('should format text with two equations (inline and separate) correctly', () => { it('should format text with two equations (inline and separate) correctly', () => {
@ -43,7 +43,7 @@ describe('TextType', () => {
}; };
// hint: katex-display is the class that indicates a separate equation // hint: katex-display is the class that indicates a separate equation
const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>'; const expectedOutput = '<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow><mi>a</mi><mo>+</mo><mi>b</mi><mo>=</mo><mi>c</mi></mrow><annotation encoding=\"application/x-tex\">a + b = c</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6667em;vertical-align:-0.0833em;\"></span><span class=\"mord mathnormal\">a</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span><span class=\"mbin\">+</span><span class=\"mspace\" style=\"margin-right:0.2222em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.6944em;\"></span><span class=\"mord mathnormal\">b</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.4306em;\"></span><span class=\"mord mathnormal\">c</span></span></span></span> ? <span class=\"katex-display\"><span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\" display=\"block\"><semantics><mrow><mi>E</mi><mo>=</mo><mi>m</mi><msup><mi>c</mi><mn>2</mn></msup></mrow><annotation encoding=\"application/x-tex\">E=mc^2</annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"><span class=\"base\"><span class=\"strut\" style=\"height:0.6833em;\"></span><span class=\"mord mathnormal\" style=\"margin-right:0.05764em;\">E</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span><span class=\"mrel\">=</span><span class=\"mspace\" style=\"margin-right:0.2778em;\"></span></span><span class=\"base\"><span class=\"strut\" style=\"height:0.8641em;\"></span><span class=\"mord mathnormal\">m</span><span class=\"mord\"><span class=\"mord mathnormal\">c</span><span class=\"msupsub\"><span class=\"vlist-t\"><span class=\"vlist-r\"><span class=\"vlist\" style=\"height:0.8641em;\"><span style=\"top:-3.113em;margin-right:0.05em;\"><span class=\"pstrut\" style=\"height:2.7em;\"></span><span class=\"sizing reset-size6 size3 mtight\"><span class=\"mord mtight\">2</span></span></span></span></span></span></span></span></span></span></span></span>';
expect(TextType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });
it('should format text with a katex matrix correctly', () => { it('should format text with a katex matrix correctly', () => {
@ -55,7 +55,7 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow></mrow><annotation encoding=\"application/x-tex\"></annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}'; const expectedOutput = 'Donnez le déterminant de la matrice suivante.<span class=\"katex\"><span class=\"katex-mathml\"><math xmlns=\"http://www.w3.org/1998/Math/MathML\"><semantics><mrow></mrow><annotation encoding=\"application/x-tex\"></annotation></semantics></math></span><span class=\"katex-html\" aria-hidden=\"true\"></span></span>\\begin{pmatrix}<br> a&b \\\\<br> c&d<br>\\end{pmatrix}';
expect(TextType({ text: input })).toContain(expectedOutput); expect(textType({ text: input })).toContain(expectedOutput);
}); });
it('should format text with Markdown correctly', () => { it('should format text with Markdown correctly', () => {
@ -65,7 +65,7 @@ describe('TextType', () => {
}; };
// TODO: investigate why the output has an extra newline // TODO: investigate why the output has an extra newline
const expectedOutput = '<strong>Bold</strong>\n'; const expectedOutput = '<strong>Bold</strong>\n';
expect(TextType({ text: input })).toBe(expectedOutput); expect(textType({ text: input })).toBe(expectedOutput);
}); });
it('should format text with HTML correctly', () => { it('should format text with HTML correctly', () => {
@ -74,7 +74,7 @@ describe('TextType', () => {
format: 'html' format: 'html'
}; };
const expectedOutput = '<em>yes</em>'; const expectedOutput = '<em>yes</em>';
expect(TextType({ text: input })).toBe(expectedOutput); expect(textType({ text: input })).toBe(expectedOutput);
}); });
it('should format plain text correctly', () => { it('should format plain text correctly', () => {
@ -83,7 +83,7 @@ describe('TextType', () => {
format: 'plain' format: 'plain'
}; };
const expectedOutput = 'Just plain text'; const expectedOutput = 'Just plain text';
expect(TextType({ text: input })).toBe(expectedOutput); expect(textType({ text: input })).toBe(expectedOutput);
}); });
// Add more tests for other formats if needed // Add more tests for other formats if needed

View file

@ -1,7 +1,7 @@
import { TemplateOptions, Description as DescriptionType } from './types'; import { TemplateOptions, Description as DescriptionType } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import { ParagraphStyle } from '../constants'; import { ParagraphStyle } from '../constants';
import { state } from '.'; import { state } from '.';
@ -14,7 +14,7 @@ export default function Description({ title, stem }: DescriptionOptions): string
type: 'Description', type: 'Description',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>` })}</p>`
] ]

View file

@ -1,7 +1,7 @@
import { TemplateOptions, Essay as EssayType } from './types'; import { TemplateOptions, Essay as EssayType } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, TextAreaStyle } from '../constants'; import { ParagraphStyle, TextAreaStyle } from '../constants';
import { state } from '.'; import { state } from '.';
@ -15,7 +15,7 @@ export default function Essay({ title, stem, globalFeedback }: EssayOptions): st
type: 'Développement', type: 'Développement',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
`<textarea class="gift-textarea" style="${TextAreaStyle( `<textarea class="gift-textarea" style="${TextAreaStyle(

View file

@ -1,5 +1,5 @@
import { TemplateOptions, Question } from './types'; import { TemplateOptions, Question } from './types';
import TextType from './TextType'; import textType from './TextType';
import { state } from '.'; import { state } from '.';
import { theme } from '../constants'; import { theme } from '../constants';
@ -21,7 +21,7 @@ export default function GlobalFeedback({ feedback }: GlobalFeedbackOptions): str
return feedback !== null return feedback !== null
? `<div style="${Container}"> ? `<div style="${Container}">
<p>${TextType({ text: feedback })}</p> <p>${textType({ text: feedback })}</p>
</div>` </div>`
: ``; : ``;
} }

View file

@ -1,7 +1,7 @@
import { TemplateOptions, Matching as MatchingType } from './types'; import { TemplateOptions, Matching as MatchingType } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, SelectStyle } from '../constants'; import { ParagraphStyle, SelectStyle } from '../constants';
import { state } from '.'; import { state } from '.';
@ -24,7 +24,7 @@ export default function Matching({
type: 'Appariement', type: 'Appariement',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
MatchAnswers({ choices: matchPairs }), MatchAnswers({ choices: matchPairs }),
@ -67,7 +67,7 @@ function MatchAnswers({ choices }: MatchAnswerOptions): string {
.map(({ subquestion }) => { .map(({ subquestion }) => {
return ` return `
<div style="${OptionTable} ${ParagraphStyle(state.theme)}"> <div style="${OptionTable} ${ParagraphStyle(state.theme)}">
${TextType({ text: subquestion })} ${textType({ text: subquestion })}
</div> </div>
<div> <div>
<select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}"> <select class="gift-select" style="${SelectStyle(state.theme)} ${Dropdown}">

View file

@ -2,7 +2,7 @@ import { TemplateOptions, MultipleChoice as MultipleChoiceType } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import MultipleChoiceAnswers from './MultipleChoiceAnswers'; import MultipleChoiceAnswers from './MultipleChoiceAnswers';
import { ParagraphStyle } from '../constants'; import { ParagraphStyle } from '../constants';
import { state } from '.'; import { state } from '.';
@ -21,7 +21,7 @@ export default function MultipleChoice({
type: 'Choix multiple', type: 'Choix multiple',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
MultipleChoiceAnswers({ choices: choices }), MultipleChoiceAnswers({ choices: choices }),

View file

@ -1,6 +1,6 @@
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { TemplateOptions, TextFormat, Choice, MultipleChoice as MultipleChoiceType } from './types'; import { TemplateOptions, TextFormat, Choice, MultipleChoice as MultipleChoiceType } from './types';
import TextType from './TextType'; import textType from './TextType';
import AnswerIcon from './AnswerIcon'; import AnswerIcon from './AnswerIcon';
import { state } from '.'; import { state } from '.';
import { ParagraphStyle, theme } from '../constants'; import { ParagraphStyle, theme } from '../constants';
@ -41,7 +41,7 @@ export default function MultipleChoiceAnswers({ choices }: MultipleChoiceAnswerO
}" id="${inputId}" name="${id}"> }" id="${inputId}" name="${id}">
${AnswerWeight({ correct: isCorrectOption, weight: weight })} ${AnswerWeight({ correct: isCorrectOption, weight: weight })}
<label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}"> <label style="${CustomLabel} ${ParagraphStyle(state.theme)}" for="${inputId}">
${TextType({ text: text as TextFormat })} ${textType({ text: text as TextFormat })}
</label> </label>
${AnswerIcon({ correct: isCorrectOption })} ${AnswerIcon({ correct: isCorrectOption })}
${AnswerFeedback({ feedback: feedback })} ${AnswerFeedback({ feedback: feedback })}
@ -85,5 +85,5 @@ function AnswerFeedback({ feedback }: AnswerFeedbackOptions): string {
color: ${theme(state.theme, 'teal700', 'gray700')}; color: ${theme(state.theme, 'teal700', 'gray700')};
`; `;
return feedback ? `<span style="${Container}">${TextType({ text: feedback })}</span>` : ``; return feedback ? `<span style="${Container}">${textType({ text: feedback })}</span>` : ``;
} }

View file

@ -1,7 +1,7 @@
import { TemplateOptions, Numerical as NumericalType, NumericalFormat } from './types'; import { TemplateOptions, Numerical as NumericalType, NumericalFormat } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, InputStyle } from '../constants'; import { ParagraphStyle, InputStyle } from '../constants';
import { state } from '.'; import { state } from '.';
@ -21,7 +21,7 @@ export default function Numerical({
type: 'Numérique', type: 'Numérique',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
NumericalAnswers({ choices: choices }), NumericalAnswers({ choices: choices }),

View file

@ -1,7 +1,7 @@
import { TemplateOptions, ShortAnswer as ShortAnswerType, TextFormat } from './types'; import { TemplateOptions, ShortAnswer as ShortAnswerType, TextFormat } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import Title from './Title'; import Title from './Title';
import TextType from './TextType'; import textType from './TextType';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import { ParagraphStyle, InputStyle } from '../constants'; import { ParagraphStyle, InputStyle } from '../constants';
import { state } from './index'; import { state } from './index';
@ -21,7 +21,7 @@ export default function ShortAnswer({
type: 'Réponse courte', type: 'Réponse courte',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
Answers({ choices: choices }), Answers({ choices: choices }),
@ -32,7 +32,7 @@ export default function ShortAnswer({
function Answers({ choices }: AnswerOptions): string { function Answers({ choices }: AnswerOptions): string {
const placeholder = choices const placeholder = choices
.map(({ text }) => TextType({ text: text as TextFormat })) .map(({ text }) => textType({ text: text as TextFormat }))
.join(', '); .join(', ');
return ` return `
<div> <div>

View file

@ -6,7 +6,7 @@ interface TextTypeOptions extends TemplateOptions {
text: TextFormat; text: TextFormat;
} }
function formatLatex(text: string): string { export function formatLatex(text: string): string {
return text return text
.replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true })) .replace(/\$\$(.*?)\$\$/g, (_, inner) => katex.renderToString(inner, { displayMode: true }))
.replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false })) .replace(/\$(.*?)\$/g, (_, inner) => katex.renderToString(inner, { displayMode: false }))
@ -28,7 +28,7 @@ function formatLatex(text: string): string {
* @see marked * @see marked
* @see katex * @see katex
*/ */
export default function TextType({ text }: TextTypeOptions) { export default function textType({ text }: TextTypeOptions) {
const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped const formatText = formatLatex(text.text.trim()); // latex needs pure "&", ">", etc. Must not be escaped
switch (text.format) { switch (text.format) {

View file

@ -1,6 +1,6 @@
import { TemplateOptions, TextChoice, TrueFalse as TrueFalseType } from './types'; import { TemplateOptions, TextChoice, TrueFalse as TrueFalseType } from './types';
import QuestionContainer from './QuestionContainer'; import QuestionContainer from './QuestionContainer';
import TextType from './TextType'; import textType from './TextType';
import GlobalFeedback from './GlobalFeedback'; import GlobalFeedback from './GlobalFeedback';
import MultipleChoiceAnswers from './MultipleChoiceAnswers'; import MultipleChoiceAnswers from './MultipleChoiceAnswers';
import Title from './Title'; import Title from './Title';
@ -44,7 +44,7 @@ export default function TrueFalse({
type: 'Vrai/Faux', type: 'Vrai/Faux',
title: title title: title
}), }),
`<p style="${ParagraphStyle(state.theme)}">${TextType({ `<p style="${ParagraphStyle(state.theme)}">${textType({
text: stem text: stem
})}</p>`, })}</p>`,
MultipleChoiceAnswers({ choices: choices }), MultipleChoiceAnswers({ choices: choices }),

View file

@ -19,8 +19,8 @@ import {
TableHead, TableHead,
TableRow TableRow
} from '@mui/material'; } from '@mui/material';
import Latex from 'react-latex';
import { UserType } from '../../Types/UserType'; import { UserType } from '../../Types/UserType';
import { formatLatex } from '../GiftTemplate/templates/TextType';
interface LiveResultsProps { interface LiveResultsProps {
socket: Socket | null; socket: Socket | null;
@ -242,16 +242,11 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</FormGroup> </FormGroup>
</div> </div>
<Table size="small" stickyHeader component={Paper}> <div className="table-container">
<Table size="small" component={Paper}>
<TableHead> <TableHead>
<TableRow> <TableRow>
<TableCell <TableCell className="sticky-column">
sx={{
borderStyle: 'solid',
borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)'
}}
>
<div className="text-base text-bold">Nom d'utilisateur</div> <div className="text-base text-bold">Nom d'utilisateur</div>
</TableCell> </TableCell>
{Array.from({ length: maxQuestions }, (_, index) => ( {Array.from({ length: maxQuestions }, (_, index) => (
@ -259,7 +254,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
key={index} key={index}
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
cursor: `pointer`, cursor: 'pointer',
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: 1, borderWidth: 1,
borderColor: 'rgba(224, 224, 224, 1)' borderColor: 'rgba(224, 224, 224, 1)'
@ -270,6 +265,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</TableCell> </TableCell>
))} ))}
<TableCell <TableCell
className="sticky-header"
sx={{ sx={{
textAlign: 'center', textAlign: 'center',
borderStyle: 'solid', borderStyle: 'solid',
@ -285,6 +281,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
{studentResults.map((student) => ( {studentResults.map((student) => (
<TableRow key={student.idUser}> <TableRow key={student.idUser}>
<TableCell <TableCell
className="sticky-column"
sx={{ sx={{
borderStyle: 'solid', borderStyle: 'solid',
borderWidth: 1, borderWidth: 1,
@ -301,6 +298,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
); );
const answerText = answer ? answer.answer.toString() : ''; const answerText = answer ? answer.answer.toString() : '';
const isCorrect = answer ? answer.isCorrect : false; const isCorrect = answer ? answer.isCorrect : false;
return ( return (
<TableCell <TableCell
key={index} key={index}
@ -319,7 +317,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
} }
> >
{showCorrectAnswers ? ( {showCorrectAnswers ? (
<Latex>{answerText}</Latex> <div>{formatLatex(answerText)}</div>
) : isCorrect ? ( ) : isCorrect ? (
<FontAwesomeIcon icon={faCheck} /> <FontAwesomeIcon icon={faCheck} />
) : ( ) : (
@ -347,7 +345,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</TableBody> </TableBody>
<TableFooter> <TableFooter>
<TableRow sx={{ backgroundColor: '#d3d3d34f' }}> <TableRow sx={{ backgroundColor: '#d3d3d34f' }}>
<TableCell sx={{ color: 'black' }}> <TableCell className="sticky-column" sx={{ color: 'black' }}>
<div className="text-base text-bold">% réussite</div> <div className="text-base text-bold">% réussite</div>
</TableCell> </TableCell>
{Array.from({ length: maxQuestions }, (_, index) => ( {Array.from({ length: maxQuestions }, (_, index) => (
@ -384,6 +382,7 @@ const LiveResults: React.FC<LiveResultsProps> = ({ socket, questions, showSelect
</TableFooter> </TableFooter>
</Table> </Table>
</div> </div>
</div>
); );
}; };

View file

@ -12,3 +12,75 @@
font-size: 2rem; font-size: 2rem;
font-weight: 500; font-weight: 500;
} }
/* Flexbox container for the action bar */
.action-bar {
display: flex;
flex-direction: column;
align-items: flex-start;
}
/* Flexbox container for the form group */
.form-group {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.table-cell-border {
border-style: solid;
border-width: 1px;
border-color: rgba(224, 224, 224, 1);
}
/* -------------- */
.table-container {
overflow-x: auto;
position: relative;
}
.sticky-header {
position: sticky;
top: 0;
background-color: white;
z-index: 2; /* Ensure it stays above other cells */
}
.sticky-column {
position: sticky;
left: 0;
background-color: white;
z-index: 1; /* Ensure it stays above other cells but below the header */
}
.sticky-intersection {
z-index: 3; /* Ensure the top-left cell stays above both header and column */
}
/* ------------------------------- */
/* Media query for narrow screens */
@media (max-width: 600px) {
.action-bar {
flex-direction: column;
align-items: stretch;
}
.form-group {
flex-direction: column;
align-items: stretch;
}
.text-2xl {
font-size: 1.5rem; /* Adjust font size for smaller screens */
}
.text-sm {
font-size: 0.875rem; /* Adjust font size for smaller screens */
}
.mb-1 {
margin-bottom: 0.5rem; /* Adjust margin for smaller screens */
}
}

View file

@ -2,9 +2,9 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import TextType from '../../GiftTemplate/templates/TextType'; import textType, { formatLatex } from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
import Latex from 'react-latex'; // import Latex from 'react-latex';
type Choices = { type Choices = {
feedback: { format: string; text: string } | null; feedback: { format: string; text: string } | null;
@ -39,7 +39,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: TextType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
{choices.map((choice, i) => { {choices.map((choice, i) => {
@ -56,7 +56,7 @@ const MultipleChoiceQuestion: React.FC<Props> = (props) => {
(choice.isCorrect ? '✅' : '❌')} (choice.isCorrect ? '✅' : '❌')}
<div className={`circle ${selected}`}>{alphabet[i]}</div> <div className={`circle ${selected}`}>{alphabet[i]}</div>
<div className={`answer-text ${selected}`}> <div className={`answer-text ${selected}`}>
<Latex>{choice.text.text}</Latex> {formatLatex(choice.text.text)}
</div> </div>
</Button> </Button>
{choice.feedback && showAnswer && ( {choice.feedback && showAnswer && (

View file

@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import TextType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
type CorrectAnswer = { type CorrectAnswer = {
@ -34,7 +34,7 @@ const NumericalQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div> <div>
<div dangerouslySetInnerHTML={{ __html: TextType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>

View file

@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button, TextField } from '@mui/material'; import { Button, TextField } from '@mui/material';
import TextType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
type Choices = { type Choices = {
@ -27,7 +27,7 @@ const ShortAnswerQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-wrapper"> <div className="question-wrapper">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: TextType({text: questionContent}) }} /> <div dangerouslySetInnerHTML={{ __html: textType({text: questionContent}) }} />
</div> </div>
{showAnswer ? ( {showAnswer ? (
<> <>

View file

@ -2,7 +2,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import '../questionStyle.css'; import '../questionStyle.css';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import TextType from '../../GiftTemplate/templates/TextType'; import textType from '../../GiftTemplate/templates/TextType';
import { TextFormat } from '../../GiftTemplate/templates/types'; import { TextFormat } from '../../GiftTemplate/templates/types';
interface Props { interface Props {
@ -27,7 +27,7 @@ const TrueFalseQuestion: React.FC<Props> = (props) => {
return ( return (
<div className="question-container"> <div className="question-container">
<div className="question content"> <div className="question content">
<div dangerouslySetInnerHTML={{ __html: TextType({ text: questionContent }) }} /> <div dangerouslySetInnerHTML={{ __html: textType({ text: questionContent }) }} />
</div> </div>
<div className="choices-wrapper mb-1"> <div className="choices-wrapper mb-1">
<Button <Button

View file

@ -151,16 +151,6 @@ const JoinRoom: React.FC = () => {
title='Rejoindre une salle' title='Rejoindre une salle'
error={connectionError}> error={connectionError}>
<TextField
label="Nom d'utilisateur"
variant="outlined"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Nom d'utilisateur"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<TextField <TextField
type="number" type="number"
label="Numéro de la salle" label="Numéro de la salle"
@ -172,6 +162,16 @@ const JoinRoom: React.FC = () => {
fullWidth fullWidth
/> />
<TextField
label="Nom d'utilisateur"
variant="outlined"
value={username}
onChange={(e) => setUsername(e.target.value)}
placeholder="Nom d'utilisateur"
sx={{ marginBottom: '1rem' }}
fullWidth
/>
<LoadingButton <LoadingButton
loading={isConnecting} loading={isConnecting}
onClick={handleSocket} onClick={handleSocket}

View file

@ -255,8 +255,8 @@ const ManageRoom: React.FC = () => {
<div className='dumb'></div> <div className='dumb'></div>
</div> </div>
{/* the following breaks the css (nested room classes) */}
<div className='room'> <div className=''>
{quizQuestions ? ( {quizQuestions ? (

View file

@ -23,6 +23,8 @@ services:
EMAIL_PSW: 'vvml wmfr dkzb vjzb' EMAIL_PSW: 'vvml wmfr dkzb vjzb'
JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe JWT_SECRET: haQdgd2jp09qb897GeBZyJetC8ECSpbFJe
FRONTEND_URL: "http://localhost:5173" FRONTEND_URL: "http://localhost:5173"
volumes:
- ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json
depends_on: depends_on:
- mongo - mongo
restart: always restart: always

1
server/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
auth_config.json

View file

@ -0,0 +1,206 @@
const request = require("supertest");
const AuthConfig = require("../config/auth.js");
const AuthManager = require("../auth/auth-manager.js");
const mockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL: "https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
{
provider2: {
type: "oidc",
OIDC_CLIENT_ID: "your_oidc_client_id",
OIDC_CLIENT_SECRET: "your_oidc_client_secret",
OIDC_ISSUER_URL: "https://your-issuer.com",
OIDC_ROLE_TEACHER_VALUE: "teacher-claim-value",
OIDC_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
"simple-login": {
enabled: true,
name: "provider3",
SESSION_SECRET: "your_session_secret",
},
},
};
// Créez une instance de AuthConfig en utilisant la configuration mockée
describe(
"AuthConfig Class Tests",
() => {
let authConfigInstance;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
authConfigInstance.loadConfigTest(mockConfig); // On injecte la configuration mockée
});
it("devrait retourner la configuration PassportJS", () => {
const config = authConfigInstance.getPassportJSConfig();
expect(config).toHaveProperty("provider1");
expect(config).toHaveProperty("provider2");
});
it("devrait retourner la configuration Simple Login", () => {
const config = authConfigInstance.getSimpleLoginConfig();
expect(config).toHaveProperty("name", "provider3");
expect(config).toHaveProperty("SESSION_SECRET", "your_session_secret");
});
it("devrait retourner les providers OAuth", () => {
const oauthProviders = authConfigInstance.getOAuthProviders();
expect(Array.isArray(oauthProviders)).toBe(true);
expect(oauthProviders.length).toBe(1); // Il y a un seul provider OAuth
expect(oauthProviders[0]).toHaveProperty("provider1");
});
it("devrait valider la configuration des providers", () => {
expect(() => authConfigInstance.validateProvidersConfig()).not.toThrow();
});
it("devrait lever une erreur si une configuration manque", () => {
const invalidMockConfig = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_CLIENT_ID: "your_oauth_client_id", // Il manque des champs nécessaires
},
},
],
},
};
const instanceWithInvalidConfig = new AuthConfig();
instanceWithInvalidConfig.loadConfigTest(invalidMockConfig);
// Vérifiez que l'erreur est lancée avec les champs manquants corrects
expect(() => instanceWithInvalidConfig.validateProvidersConfig()).toThrow(
new Error(`Configuration invalide pour les providers suivants : [
{
"provider": "provider1",
"missingFields": [
"OAUTH_AUTHORIZATION_URL",
"OAUTH_TOKEN_URL",
"OAUTH_USERINFO_URL",
"OAUTH_CLIENT_SECRET",
"OAUTH_ROLE_TEACHER_VALUE",
"OAUTH_ROLE_STUDENT_VALUE"
]
}
]`)
);
});
},
describe("Auth Module Registration", () => {
let expressMock = jest.mock("express");
expressMock.use = () => {}
expressMock.get = () => {}
let authConfigInstance;
let logSpy;
// Initialisez l'instance avec la configuration mockée
beforeAll(() => {
authConfigInstance = new AuthConfig();
});
it("should load valid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModule = {
auth: {
passportjs: [
{
provider1: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
provider2: {
type: "oauth",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(0);
logSpy.mockClear();
});
it("should not load invalid modules", () => {
const logSpy = jest.spyOn(global.console, "error");
const invalidModule = {
auth: {
ModuleX:{}
},
};
authConfigInstance.loadConfigTest(invalidModule); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(1);
logSpy.mockClear();
});
it("should not load invalid provider from passport", () => {
const logSpy = jest.spyOn(global.console, "error");
const validModuleInvalidProvider = {
auth: {
passportjs: [
{
provider1: {
type: "x",
OAUTH_AUTHORIZATION_URL:
"https://www.testurl.com/oauth2/authorize",
OAUTH_TOKEN_URL: "https://www.testurl.com/oauth2/token",
OAUTH_USERINFO_URL: "https://www.testurl.com/oauth2/userinfo/",
OAUTH_CLIENT_ID: "your_oauth_client_id",
OAUTH_CLIENT_SECRET: "your_oauth_client_secret",
OAUTH_ADD_SCOPE: "scopes",
OAUTH_ROLE_TEACHER_VALUE: "teacher-claim-value",
OAUTH_ROLE_STUDENT_VALUE: "student-claim-value",
},
},
],
},
};
authConfigInstance.loadConfigTest(validModuleInvalidProvider); // On injecte la configuration mockée
authmanagerInstance = new AuthManager(expressMock,authConfigInstance.config);
expect(logSpy).toHaveBeenCalledTimes(2);
logSpy.mockClear();
});
})
);

View file

@ -12,6 +12,8 @@ const userRouter = require('./routers/users.js');
const folderRouter = require('./routers/folders.js'); const folderRouter = require('./routers/folders.js');
const quizRouter = require('./routers/quiz.js'); const quizRouter = require('./routers/quiz.js');
const imagesRouter = require('./routers/images.js') const imagesRouter = require('./routers/images.js')
const AuthManager = require('./auth/auth-manager.js')
const authRouter = require('./routers/auth.js')
// Setup environement // Setup environement
dotenv.config(); dotenv.config();
@ -48,6 +50,18 @@ app.use('/api/user', userRouter);
app.use('/api/folder', folderRouter); app.use('/api/folder', folderRouter);
app.use('/api/quiz', quizRouter); app.use('/api/quiz', quizRouter);
app.use('/api/image', imagesRouter); app.use('/api/image', imagesRouter);
app.use('/api/auth', authRouter);
// Add Auths methods
const session = require('express-session');
app.use(session({
secret: process.env['SESSION_Secret'],
resave: false,
saveUninitialized: false,
cookie: { secure: process.env.NODE_ENV === 'production' }
}));
authManager = new AuthManager(app)
app.use(errorHandler) app.use(errorHandler)

View file

@ -0,0 +1,57 @@
const fs = require('fs');
const AuthConfig = require('../config/auth.js');
class AuthManager{
constructor(expressapp,configs=null){
this.modules = []
this.app = expressapp
this.configs = configs ?? (new AuthConfig()).loadConfig()
this.addModules()
this.registerAuths()
}
async addModules(){
for(const module in this.configs.auth){
this.addModule(module)
}
}
async addModule(name){
const modulePath = `${process.cwd()}/auth/modules/${name}.js`
if(fs.existsSync(modulePath)){
const Module = require(modulePath);
this.modules.push(new Module(this,this.configs.auth[name]));
console.info(`Module d'authentification '${name}' ajouté`)
} else{
console.error(`Le module d'authentification ${name} n'as pas été chargé car il est introuvable`)
}
}
async registerAuths(){
for(const module of this.modules){
try{
module.registerAuth(this.app)
} catch(error){
console.error(`L'enregistrement du module ${module} a échoué.`)
}
}
}
async login(userInfos){
// TODO global user login method
console.log(userInfos)
}
async register(userInfos){
// TODO global user register method
console.log(userInfos)
}
async logout(){
// TODO global user logout method
}
}
module.exports = AuthManager;

View file

@ -0,0 +1,66 @@
var OAuth2Strategy = require('passport-oauth2')
class PassportOAuth {
register(app, passport,endpoint, name, provider) {
passport.use(name, new OAuth2Strategy({
authorizationURL: provider.OAUTH_AUTHORIZATION_URL,
tokenURL: provider.OAUTH_TOKEN_URL,
clientID: provider.OAUTH_CLIENT_ID,
clientSecret: provider.OAUTH_CLIENT_SECRET,
callbackURL: `${endpoint}/${name}/callback`,
passReqToCallback: true
},
async function(req, accessToken, refreshToken, params, profile, done) {
try {
const userInfoResponse = await fetch(provider.OAUTH_USERINFO_URL, {
headers: { 'Authorization': `Bearer ${accessToken}` }
});
const userInfo = await userInfoResponse.json();
const user = {
id: userInfo.sub,
email: userInfo.email,
name: userInfo.name,
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: params.expires_in
};
// Store the tokens in the session
req.session.oauth2Tokens = {
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: params.expires_in
};
return done(null, user);
} catch (error) {
console.error(`Erreur dans la strategie OAuth2 '${name}' : ${error}`);
return done(error);
}
}));
app.get(`${endpoint}/${name}`, (req, res, next) => {
passport.authenticate(name, {
scope: 'openid profile email offline_access'+ ` ${provider.OAUTH_ADD_SCOPE}`,
prompt: 'consent'
})(req, res, next);
});
app.get(`${endpoint}/${name}/callback`,
(req, res, next) => {
passport.authenticate(name, { failureRedirect: '/login' })(req, res, next);
},
(req, res) => {
if (req.user) {
res.json(req.user)
console.info(`L'utilisateur '${req.user.name}' vient de se connecter`)
} else {
res.status(401).json({ error: "L'authentification a échoué" });
}
}
);
}
}
module.exports = PassportOAuth;

View file

@ -0,0 +1,51 @@
const fs = require('fs');
var passport = require('passport')
class PassportJs{
constructor(authmanager,settings){
this.authmanager = authmanager
this.registeredProviders = {}
this.providers = settings
this.endpoint = "/api/auth"
}
registerAuth(expressapp){
expressapp.use(passport.initialize());
expressapp.use(passport.session());
for(const p of this.providers){
for(const [name,provider] of Object.entries(p)){
if(!(provider.type in this.registeredProviders)){
this.registerProvider(provider.type)
}
try{
this.registeredProviders[provider.type].register(expressapp,passport,this.endpoint,name,provider)
} catch(error){
console.error(`La connexion ${name} de type ${provider.type} n'as pu être chargé.`)
}
}
}
passport.serializeUser(function(user, done) {
done(null, user);
});
passport.deserializeUser(function(user, done) {
done(null, user);
});
}
registerProvider(providerType){
try{
const providerPath = `${process.cwd()}/auth/modules/passport-providers/${providerType}.js`
const Provider = require(providerPath);
this.registeredProviders[providerType]= new Provider()
console.info(`Le type de connexion '${providerType}' a été ajouté dans passportjs.`)
} catch(error){
console.error(`Le type de connexion '${providerType}' n'as pas pu être chargé dans passportjs.`)
}
}
}
module.exports = PassportJs;

View file

@ -0,0 +1,28 @@
{
"auth": {
"passportjs":
[
{
"gmatte": {
"type": "oauth",
"OAUTH_AUTHORIZATION_URL": "https://auth.gmatte.xyz/application/o/authorize/",
"OAUTH_TOKEN_URL": "https://auth.gmatte.xyz/application/o/token/",
"OAUTH_USERINFO_URL": "https://auth.gmatte.xyz/application/o/userinfo/",
"OAUTH_CLIENT_ID": "clientID",
"OAUTH_CLIENT_SECRET": "clientSecret",
"OAUTH_ADD_SCOPE": "groups",
"OAUTH_ROLE_TEACHER_VALUE": "groups_evaluetonsavoir-prof",
"OAUTH_ROLE_STUDENT_VALUE": "groups_evaluetonsavoir"
}
},
{
"oidc":{
"type":"oidc"
}
}
],
"Module X":{
}
}
}

185
server/config/auth.js Normal file
View file

@ -0,0 +1,185 @@
const fs = require('fs');
const path = require('path');
const pathAuthConfig = './auth_config.json';
const configPath = path.join(process.cwd(), pathAuthConfig);
class AuthConfig {
config = null;
// Méthode pour lire le fichier de configuration JSON
loadConfig() {
try {
const configData = fs.readFileSync(configPath, 'utf-8');
this.config = JSON.parse(configData);
return this.config
} catch (error) {
console.error("Erreur lors de la lecture du fichier de configuration :", error);
return null;
}
}
// Méthode pour load le fichier de test
loadConfigTest(mockConfig) {
this.config = mockConfig;
}
// Méthode pour retourner la configuration des fournisseurs PassportJS
getPassportJSConfig() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const passportConfig = {};
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
passportConfig[providerName] = provider[providerName];
});
return passportConfig;
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner la configuration de Simple Login
getSimpleLoginConfig() {
if (this.config && this.config.auth && this.config.auth["simple-login"]) {
return this.config.auth["simple-login"];
} else {
return { error: "Aucune configuration Simple Login disponible." };
}
}
// Méthode pour retourner tous les providers de type OAuth
getOAuthProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oauthProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oauth';
});
if (oauthProviders.length > 0) {
return oauthProviders;
} else {
return { error: "Aucun fournisseur OAuth disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour retourner tous les providers de type OIDC
getOIDCProviders() {
if (this.config && this.config.auth && this.config.auth.passportjs) {
const oidcProviders = this.config.auth.passportjs.filter(provider => {
const providerName = Object.keys(provider)[0];
return provider[providerName].type === 'oidc';
});
if (oidcProviders.length > 0) {
return oidcProviders;
} else {
return { error: "Aucun fournisseur OIDC disponible." };
}
} else {
return { error: "Aucune configuration PassportJS disponible." };
}
}
// Méthode pour vérifier si tous les providers ont les variables nécessaires
validateProvidersConfig() {
const requiredOAuthFields = [
'OAUTH_AUTHORIZATION_URL', 'OAUTH_TOKEN_URL','OAUTH_USERINFO_URL', 'OAUTH_CLIENT_ID', 'OAUTH_CLIENT_SECRET', 'OAUTH_ROLE_TEACHER_VALUE', 'OAUTH_ROLE_STUDENT_VALUE'
];
const requiredOIDCFields = [
'OIDC_CLIENT_ID', 'OIDC_CLIENT_SECRET', 'OIDC_ISSUER_URL', 'OIDC_ROLE_TEACHER_VALUE', 'OIDC_ROLE_STUDENT_VALUE'
];
const missingFieldsReport = [];
if (this.config && this.config.auth && this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
let missingFields = [];
// Vérification des providers de type OAuth
if (providerConfig.type === 'oauth') {
missingFields = requiredOAuthFields.filter(field => !(field in providerConfig));
}
// Vérification des providers de type OIDC
else if (providerConfig.type === 'oidc') {
missingFields = requiredOIDCFields.filter(field => !(field in providerConfig));
}
// Si des champs manquent, on les ajoute au rapport
if (missingFields.length > 0) {
missingFieldsReport.push({
provider: providerName,
missingFields: missingFields
});
}
});
// Si des champs manquent, lever une exception
if (missingFieldsReport.length > 0) {
throw new Error(`Configuration invalide pour les providers suivants : ${JSON.stringify(missingFieldsReport, null, 2)}`);
} else {
console.log("Configuration auth_config.json: Tous les providers ont les variables nécessaires.")
return { success: "Tous les providers ont les variables nécessaires." };
}
} else {
throw new Error("Aucune configuration PassportJS disponible.");
}
}
// Méthode pour retourner la configuration des fournisseurs PassportJS pour le frontend
getActiveAuth() {
if (this.config && this.config.auth) {
const passportConfig = {};
// Gestion des providers PassportJS
if (this.config.auth.passportjs) {
this.config.auth.passportjs.forEach(provider => {
const providerName = Object.keys(provider)[0];
const providerConfig = provider[providerName];
passportConfig[providerName] = {};
if (providerConfig.type === 'oauth') {
passportConfig[providerName] = {
type: providerConfig.type,
authorizationUrl: providerConfig.OAUTH_AUTHORIZATION_URL,
callbackUrl: providerConfig.OAUTH_CALLBACK_URL,
};
} else if (providerConfig.type === 'oidc') {
passportConfig[providerName] = {
type: providerConfig.type,
issuerUrl: providerConfig.OIDC_ISSUER_URL,
callbackUrl: providerConfig.OIDC_CALLBACK_URL
};
}
});
}
// Gestion du Simple Login
if (this.config.auth["simple-login"] && this.config.auth["simple-login"].enabled) {
passportConfig['simple-login'] = {
type: "simple-login",
name: this.config.auth["simple-login"].name
};
}
return passportConfig;
} else {
return { error: "Aucune configuration d'authentification disponible." };
}
}
}
module.exports = AuthConfig;

View file

@ -0,0 +1,25 @@
const AuthConfig = require('../config/auth.js');
class authController {
async getActive(req, res, next) {
try {
const authC = new AuthConfig();
authC.loadConfig();
const authActive = authC.getActiveAuth();
const response = {
authActive
};
return res.json(response);
}
catch (error) {
return next(error); // Gérer l'erreur
}
}
}
module.exports = new authController;

View file

@ -13,7 +13,7 @@ class Folders {
const existingFolder = await foldersCollection.findOne({ title: title, userId: userId }); const existingFolder = await foldersCollection.findOne({ title: title, userId: userId });
if (existingFolder) return null; if (existingFolder) return new Error('Folder already exists');
const newFolder = { const newFolder = {
userId: userId, userId: userId,

View file

@ -40,7 +40,7 @@ class Users {
await userCollection.insertOne(newUser); await userCollection.insertOne(newUser);
const folderTitle = 'Dossier par Défaut'; const folderTitle = 'Dossier par Défaut';
const userId = newUser._id; const userId = newUser._id.toString();
await Folders.create(folderTitle, userId); await Folders.create(folderTitle, userId);
// TODO: verif if inserted properly... // TODO: verif if inserted properly...

129
server/package-lock.json generated
View file

@ -13,10 +13,14 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openid-oauth20": "^1.2.6",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },
@ -1589,6 +1593,14 @@
"node": "^4.5.0 || >= 5.9" "node": "^4.5.0 || >= 5.9"
} }
}, },
"node_modules/base64url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz",
"integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/bcrypt": { "node_modules/bcrypt": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.1.tgz",
@ -2535,6 +2547,29 @@
"node": ">= 0.10.0" "node": ">= 0.10.0"
} }
}, },
"node_modules/express-session": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.0.tgz",
"integrity": "sha512-m93QLWr0ju+rOwApSsyso838LQwgfs44QtOP/WBiwtAgPIo/SAh1a5c6nn2BR6mFNZehTpqKDESzP+fRHVbxwQ==",
"dependencies": {
"cookie": "0.6.0",
"cookie-signature": "1.0.7",
"debug": "2.6.9",
"depd": "~2.0.0",
"on-headers": "~1.0.2",
"parseurl": "~1.3.3",
"safe-buffer": "5.2.1",
"uid-safe": "~2.1.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/express-session/node_modules/cookie-signature": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA=="
},
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
@ -4492,6 +4527,11 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"node_modules/oauth": {
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.0.tgz",
"integrity": "sha512-1orQ9MT1vHFGQxhuy7E/0gECD3fd2fCC+PIX+/jgmU/gI3EpRocXtmtvxCO5x3WZ443FLTLFWNDjl5MPJf9u+Q=="
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@ -4522,6 +4562,14 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/on-headers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
"integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
@ -4607,6 +4655,58 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/passport": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/passport/-/passport-0.7.0.tgz",
"integrity": "sha512-cPLl+qZpSc+ireUvt+IzqbED1cHHkDoVYMo30jbJIdOOjQ1MQYZBPiNvmi8UM6lJuOpTPXJGZQk0DtC4y61MYQ==",
"dependencies": {
"passport-strategy": "1.x.x",
"pause": "0.0.1",
"utils-merge": "^1.0.1"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-oauth2": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"dependencies": {
"base64url": "3.x.x",
"oauth": "0.10.x",
"passport-strategy": "1.x.x",
"uid2": "0.0.x",
"utils-merge": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/jaredhanson"
}
},
"node_modules/passport-openid-oauth20": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/passport-openid-oauth20/-/passport-openid-oauth20-1.2.6.tgz",
"integrity": "sha512-L9OMSH/sT73gvk0TLU2UaWb1Gk5KqQB4c9penDTtpZGw6czzznaiA+xPzOAygGtqAIcfQXbW0d3e/UItxjoODQ==",
"dependencies": {
"passport-oauth2": "^1.5.0"
}
},
"node_modules/passport-strategy": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz",
"integrity": "sha512-CB97UUvDKJde2V0KDWWB3lyf6PC3FaZP7YxZ2G8OAtn9p4HI9j9JLP9qjOGZFvyl8uwNT8qM+hGnz/n16NI7oA==",
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/path-exists": { "node_modules/path-exists": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@ -4644,6 +4744,11 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w=="
}, },
"node_modules/pause": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz",
"integrity": "sha512-KG8UEiEVkR3wGEb4m5yZkVCzigAD+cVEJck2CzYZO37ZGJfctvVptVO192MwrtPhzONn6go8ylnOdMhKqi4nfg=="
},
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
@ -4783,6 +4888,14 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/random-bytes": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz",
"integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/range-parser": { "node_modules/range-parser": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@ -5571,6 +5684,22 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA=="
}, },
"node_modules/uid-safe": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz",
"integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==",
"dependencies": {
"random-bytes": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/uid2": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/uid2/-/uid2-0.0.4.tgz",
"integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA=="
},
"node_modules/undefsafe": { "node_modules/undefsafe": {
"version": "2.0.5", "version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",

View file

@ -17,10 +17,14 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.4", "dotenv": "^16.4.4",
"express": "^4.18.2", "express": "^4.18.2",
"express-session": "^1.18.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"mongodb": "^6.3.0", "mongodb": "^6.3.0",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.9", "nodemailer": "^6.9.9",
"passport": "^0.7.0",
"passport-oauth2": "^1.8.0",
"passport-openid-oauth20": "^1.2.6",
"socket.io": "^4.7.2", "socket.io": "^4.7.2",
"socket.io-client": "^4.7.2" "socket.io-client": "^4.7.2"
}, },

9
server/routers/auth.js Normal file
View file

@ -0,0 +1,9 @@
const express = require('express');
const router = express.Router();
const jwt = require('../middleware/jwtToken.js');
const authController = require('../controllers/auth.js')
router.get("/getActiveAuth",jwt.authenticate, authController.getActive);
module.exports = router;