diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..9a4de614 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# secrets +.env + +# misc +.vscode +.DS_Store +.env* +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 00000000..985a2672 --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,66 @@ +name: build docker container + +on: + push: + branches: + - master + - staging + tags: + - "v*.*.*" + pull_request: + workflow_dispatch: + +permissions: + actions: read + contents: read + +env: + DOCKERHUB_ORG: 'scribear' + +jobs: + build-container: + runs-on: ubuntu-latest + steps: + - name: Set up Git repository + uses: actions/checkout@v4 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.DOCKERHUB_ORG }}/frontend + 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: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + platforms: "linux/amd64,linux/arm64" + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.DOCKERHUB_ORG }}/frontend:buildcache + cache-to: type=registry,ref=${{ env.DOCKERHUB_ORG }}/frontend:buildcache,mode=max + build-args: | + BRANCH=${{ steps.meta.outputs.version }} + BUILDNUMBER=${{ github.run_number }} + ARG GITSHA1=${{ github.sha }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..893d502e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,20 @@ +FROM node:20 AS builder + +WORKDIR /app + +COPY package*.json . + +RUN npm install + +COPY . . + +RUN npm run build + +FROM nginx:1.27 + +WORKDIR /app + +COPY --from=builder /app/build . +COPY nginx.conf /etc/nginx/conf.d/default.conf + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..ac8b7a80 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,14 @@ +server { + listen 80; + + root /app; + index index.html index.htm; + + location / { + try_files $uri /index.html; + } + + location ~ ^/v/latest(.*) { + try_files $1 /index.html; + } +} \ No newline at end of file diff --git a/src/components/navbars/topbar/apiDropdown.tsx b/src/components/navbars/topbar/apiDropdown.tsx index 6ce8e5b1..76111c2e 100644 --- a/src/components/navbars/topbar/apiDropdown.tsx +++ b/src/components/navbars/topbar/apiDropdown.tsx @@ -26,22 +26,26 @@ export default function ApiDropdown(props) { const open = Boolean(anchorEl); const dispatch = useDispatch(); - const urlParams = new URLSearchParams(window.location.search); - const mode = urlParams.get('mode'); - const serverAddress = urlParams.get('serverAddress'); - const accessToken = urlParams.get('accessToken'); - const apiStatus = useSelector((state: RootState) => state.APIStatusReducer as ApiStatus); const scribearServerStatus = useSelector((state: RootState) => { return state.ScribearServerReducer as ScribearServerStatus }) - function switchToScribeARServer(scribearServerAddress: string) { + const urlParams = new URLSearchParams(window.location.search); + const mode = urlParams.get('mode'); + const kioskServerAddress = urlParams.get('kioskServerAddress'); + const serverAddress = urlParams.get('serverAddress'); + const accessToken = urlParams.get('accessToken'); + const sourceToken = urlParams.get('sourceToken'); + + // Automatically use scribear server as sink when in student mode or as sourcesink if in kiosk mode + useEffect(() => { + function switchToScribeARServer(scribearServerAddress: string) { // Set new scribear server address let copyScribearServerStatus = Object.assign({}, scribearServerStatus); copyScribearServerStatus.scribearServerAddress = scribearServerAddress - dispatch({type: 'CHANGE_SCRIBEAR_SERVER_ADDRESS', payload: copyScribearServerStatus}); + dispatch({type: 'CHANGE_SCRIBEAR_SERVER_ADDRESS', payload: {scribearServerAddress}}); // Switch to scribear server let copyStatus = Object.assign({}, apiStatus); @@ -56,11 +60,9 @@ export default function ApiDropdown(props) { dispatch({type: 'CHANGE_API_STATUS', payload: copyStatus}); } - // Automatically use scribear server as sink when in student mode or as sourcesink if in kiosk mode - useEffect(() => { if (mode === 'kiosk') { - switchToScribeARServer(`ws://${serverAddress}/sourcesink`); - } else if (mode === 'student' && accessToken) { + switchToScribeARServer(`ws://${kioskServerAddress}/sourcesink?sourceToken=${sourceToken}`); + } else if (mode === 'student') { console.log("Sending startSession POST with accessToken:", accessToken); fetch(`http://${serverAddress}/startSession`, { method: 'POST', @@ -81,7 +83,7 @@ export default function ApiDropdown(props) { console.error('Error starting session:', error); }); } - }, [mode, serverAddress, accessToken]); + }, [accessToken, dispatch, mode, kioskServerAddress, serverAddress, sourceToken]); const isMobile = useMediaQuery(currTheme.breakpoints.down('sm')); diff --git a/src/components/navbars/topbar/qrCodeScreen.tsx b/src/components/navbars/topbar/qrCodeScreen.tsx index 01cca70d..040c07f7 100644 --- a/src/components/navbars/topbar/qrCodeScreen.tsx +++ b/src/components/navbars/topbar/qrCodeScreen.tsx @@ -12,20 +12,36 @@ export default function QRCodeComponent() { const urlParams = new URLSearchParams(window.location.search); const isKioskMode = urlParams.get('mode') === 'kiosk'; - const serverAddress = urlParams.get('serverAddress'); + const kioskServerAddress = urlParams.get('kioskServerAddress'); const isStudentMode = urlParams.get('mode') === 'student'; + const scribearURLParam = urlParams.get('scribearURL'); + const sourceToken = urlParams.get('sourceToken'); useEffect(() => { if (!isKioskMode) return; setLoading(true); - setScribearURL(window.location.protocol + "//" + window.location.host + window.location.pathname); + if (scribearURLParam) { + setScribearURL(scribearURLParam); + } else { + setScribearURL(window.location.protocol + "//" + window.location.host + window.location.pathname); + } let updateAccessTokenTimeout; function updateAccessToken() { - fetch(`http://${serverAddress}/accessToken`) + fetch(`http://${kioskServerAddress}/accessToken`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ sourceToken }) + }) .then(response => response.json()) .then(data => { + if (!data.accessToken) { + throw Error('Unsuccessful request:' + JSON.stringify(data)); + } + setNodeServerAddress(data.serverAddress); setAccessToken(data.accessToken); setSuccessful(true); @@ -41,6 +57,10 @@ export default function QRCodeComponent() { .catch(error => { setSuccessful(false); console.error("Failed to fetch access token:", error) + + updateAccessTokenTimeout = setTimeout(() => { + updateAccessToken() + }, 30_000); }) .finally(() => setLoading(false)); } @@ -50,7 +70,7 @@ export default function QRCodeComponent() { return () => { clearTimeout(updateAccessTokenTimeout) } - }, []); + }, [isKioskMode, scribearURLParam, kioskServerAddress, sourceToken]); if (!isKioskMode) return <>;