├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── deploy ├── docker-local.sh ├── env.example ├── moderated.code-workspace ├── mvnw ├── mvnw.cmd ├── package-lock.json ├── package.json ├── pom.xml ├── public ├── assets │ ├── 8x8.svg │ ├── appstore.png │ ├── bookmark.svg │ ├── copy.svg │ ├── facebook.svg │ ├── fdroid.png │ ├── github.svg │ ├── jitsi-logo.svg │ ├── linkedin.svg │ ├── playstore.png │ └── twitter.svg └── index.html ├── run.sh ├── src ├── main │ ├── java │ │ └── org │ │ │ └── jitsi │ │ │ └── moderated │ │ │ ├── Application.java │ │ │ ├── Config.java │ │ │ ├── Constants.java │ │ │ ├── JwtTokenFilter.java │ │ │ ├── SecurityConfig.java │ │ │ ├── ServerConfig.java │ │ │ ├── controller │ │ │ └── ModeratedRoomFactory.java │ │ │ ├── endpoints │ │ │ ├── ClientConfigEndpoint.java │ │ │ └── RoomsEndpoint.java │ │ │ ├── jwt │ │ │ ├── JsonKeyProvider.java │ │ │ ├── JwtAuthentication.java │ │ │ └── UserInfo.java │ │ │ └── model │ │ │ ├── ClientConfig.java │ │ │ ├── JoinInfo.java │ │ │ └── ModeratedRoom.java │ ├── js │ │ ├── Application.tsx │ │ ├── components │ │ │ ├── ConfigContext.tsx │ │ │ └── Screen.tsx │ │ ├── functions │ │ │ ├── analytics.ts │ │ │ ├── logger.ts │ │ │ ├── restUtils.ts │ │ │ └── urlUtils.ts │ │ ├── index.tsx │ │ ├── model │ │ │ └── Config.ts │ │ ├── screens │ │ │ ├── Home.tsx │ │ │ └── Join.tsx │ │ ├── tsconfig.json │ │ └── typings │ │ │ └── Logger.d.ts │ ├── resources │ │ └── application.properties │ └── scss │ │ ├── index.scss │ │ ├── reset.css │ │ ├── ui-kit.scss │ │ └── variables.scss └── test │ └── java │ └── org │ └── jitsi │ └── moderated │ └── ApplicationTests.java ├── start.sh └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | env: { 5 | node: true 6 | }, 7 | parserOptions: { 8 | ecmaVersion: 2020, 9 | sourceType: 'module' 10 | }, 11 | plugins: [ 12 | '@typescript-eslint' 13 | ], 14 | extends: [ 15 | '@jitsi/eslint-config' 16 | ], 17 | 'globals': { 18 | 'NodeJS': true 19 | }, 20 | 'rules': { 21 | 'no-unused-vars': 'off', 22 | '@typescript-eslint/no-unused-vars': 'error' 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | !**/src/main/** 5 | !**/src/test/** 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | .sts4-cache 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | 30 | ### Maven ### 31 | target/ 32 | pom.xml.tag 33 | pom.xml.releaseBackup 34 | pom.xml.versionsBackup 35 | pom.xml.next 36 | release.properties 37 | dependency-reduced-pom.xml 38 | buildNumber.properties 39 | .mvn/timing.properties 40 | .mvn/wrapper/maven-wrapper.jar 41 | 42 | ### VS Code ### 43 | .vscode/ 44 | 45 | ### JS ### 46 | node_modules 47 | public/app.js 48 | 49 | ### Locals ### 50 | deploy-local.sh 51 | .DS_Store 52 | .env 53 | *.der 54 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye as builder 2 | 3 | RUN apt-get update && apt-get install -y curl apt-utils 4 | RUN curl -fsSL https://deb.nodesource.com/setup_16.x | bash - 5 | RUN apt-get update 6 | RUN apt-get install --no-install-recommends -y maven openjdk-11-jdk nodejs 7 | 8 | WORKDIR /opt/moderated-meetings 9 | COPY . . 10 | RUN mvn package 11 | RUN npm install && npm run build 12 | 13 | FROM openjdk:11 14 | 15 | RUN apt-get update && apt-get install --no-install-recommends -y coreutils jq 16 | 17 | WORKDIR /apps 18 | COPY --from=builder /opt/moderated-meetings/target/*.jar ./moderated-meetings.jar 19 | COPY --from=builder /opt/moderated-meetings/public ./public 20 | 21 | COPY run.sh / 22 | CMD ["/run.sh"] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Jitsi Moderated Meetings microservice 3 | 4 | Example of a standalone microservice that can generate tokens for Jitsi 5 | Moderated Meetings. 6 | 7 | ## Usage 8 | 9 | 1. ``npm install`` 10 | 2. Generate a keypair to sign JWT tokens. The public key will be used by 11 | `prosody` to validated moderated tokens, as described in: 12 | https://github.com/jitsi/lib-jitsi-meet/blob/master/doc/tokens.md 13 | 3. Create a `.env` file with environment variables in the following format: 14 | ``` 15 | DEPLOYMENT_URL= 16 | PORT= 17 | PRIVATE_KEY_FILE= 18 | PRIVATE_KEY_ID= 19 | TARGET_TENANT= 20 | ``` 21 | 22 | To generate the der file used for `PRIVATE_KEY_FILE`: 23 | ``` 24 | openssl rsa -inform pem -in jitsi-private.pem -outform der -out PrivateKey.der 25 | ``` 26 | 27 | 4. ``npm start`` 28 | 5. Open ``http://localhost:[PORT]/`` 29 | 30 | Note: 31 | - please see package.json for further npm run scripts. 32 | - the `npm start` script expects the `.env` file to exist, but the app can be ran with out that too. For details, see the `start` script in `package.json`. 33 | 34 | 35 | ## Local Image Testing 36 | 37 | The `./docker-local.sh` script is used for local testing in a Docker image. It 38 | expects the certificate file in the project root, named `moderated.der` (and it 39 | has to be reflected in the env variables too), and also expects the `.env` file 40 | to exist. 41 | 42 | 43 | ## Deployment 44 | 45 | The `deploy` script can be used to build and deploy a docker image to AWS ECR. 46 | Rigging associated with such a deployment is left as an exercise for the reader. 47 | 48 | Usage is ``./deploy.sh [version] [ecr_registry] [[ecr_region]]`` 49 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | ## Reporting security issuess 4 | 5 | We take security very seriously and develop all Jitsi projects to be secure and safe. 6 | 7 | If you find (or simply suspect) a security issue in any of the Jitsi projects, please send us an email to security@jitsi.org. 8 | 9 | **We encourage responsible disclosure for the sake of our users, so please reach out before posting in a public space.** 10 | -------------------------------------------------------------------------------- /deploy: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | set -x 4 | 5 | SERVICE_NAME=moderated-meetings 6 | 7 | VERSION=$1 8 | ECR_REGISTRY=$2 9 | ECR_REGION=$3 10 | 11 | if [[ -z "$VERSION" || -z "$ECR_REGISTRY" ]]; then 12 | echo "usage:" 13 | echo "./deploy.sh [version] [ecr_registry] [[ecr_region]]" 14 | exit 1 15 | fi 16 | 17 | if [[ -z "$ECR_REGION" ]]; then 18 | ECR_REGION=us-west-2 19 | fi 20 | 21 | echo "building docker image" 22 | docker build --platform linux/amd64 --build-arg JAR_VERSION=${VERSION} -t ${ECR_REGISTRY}/jitsi/${SERVICE_NAME}:${VERSION} . 23 | echo "pushing docker image to ecr" 24 | aws ecr get-login-password --region ${ECR_REGION} | docker login --username AWS --password-stdin ${ECR_REGISTRY} 25 | docker push ${ECR_REGISTRY}/jitsi/${SERVICE_NAME}:${VERSION} 26 | -------------------------------------------------------------------------------- /docker-local.sh: -------------------------------------------------------------------------------- 1 | # local test script 2 | docker build --tag moderated:SNAPSHOT . 3 | docker run --rm --publish 8001:8080 --name moderated-meetings --mount type=bind,source=$PWD/moderated.der,target=/usr/share/moderated-meetings/moderated.der --env-file .env moderated:SNAPSHOT 4 | -------------------------------------------------------------------------------- /env.example: -------------------------------------------------------------------------------- 1 | # The deployment url e.g. https://my.domain.com/ (required) 2 | DEPLOYMENT_URL= 3 | 4 | # The port on which the moderated meetings webserver will listen 5 | # PORT=8080 6 | 7 | # The private key file that will be used to sign tokens e.g. jitsi-private.der (required) 8 | PRIVATE_KEY_FILE= 9 | 10 | # The kid value that will be used for the jwt token header (required) 11 | PRIVATE_KEY_ID= 12 | 13 | # The tenant to be used for the moderated meetings, or can be skipped then the deployment will be used without any tenant 14 | # TARGET_TENANT=moderated 15 | 16 | # If enabled the users will be redirected to external service for authentication which will 17 | # return back to moderated meetings providing a token via url param (?jwt=). Supports url param state 18 | # TOKEN_AUTH_URL=https://my.auth.service.com/signin.html?state={state} 19 | 20 | # The URL which can be used to download the public keys that will validate the jwt token 21 | # that is provided by the authentication service from TOKEN_AUTH_URL. 22 | # The endpoint provides a json file where kid is matched to a certificate file in pem format 23 | # JWT_PUB_KEYS_CACHE_URL= 24 | 25 | # A json with additional values to be checked when verifying the token provided by the authentication service TOKEN_AUTH_URL 26 | # issuer and audience is supported e.g. 27 | # JWT_VERIFY='{"issuer":"jitsi","audience":"meet-jitsi"}' 28 | # JWT_VERIFY= 29 | 30 | # Amplitude key to be used for analytics from the client 31 | # AMPLITUDE_KEY= 32 | 33 | # Custom Facebook link to be used 34 | # FB_LINK=https://www.facebook.com/jitsi 35 | 36 | # Custom Github link to be used 37 | # GITHUB_LINK=https://github.com/jitsi 38 | 39 | # Custom Linkedin link to be used 40 | # LINKED_IN_LINK=https://www.linkedin.com/groups/133669 41 | 42 | # Custom Twitter link to be used 43 | # TWITTER_LINK=https://twitter.com/jitsinews 44 | 45 | # Custom App Store link to be used 46 | # APP_STORE_LINK=https://apps.apple.com/us/app/jitsi-meet/id1165103905 47 | 48 | # Custom F-Droid link to be used 49 | # FDRIOD_LINK=https://f-droid.org/en/packages/org.jitsi.meet 50 | 51 | # Custom Play Store link to be used 52 | # PLAY_STORE_LINK=https://play.google.com/store/apps/details?id=org.jitsi.meet 53 | 54 | -------------------------------------------------------------------------------- /moderated.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "java.configuration.updateBuildConfiguration": "disabled" 9 | } 10 | } -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jitsi-moderated-meetings", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/main/js/index.js", 6 | "private": true, 7 | "scripts": { 8 | "build": "webpack", 9 | "lint": "eslint --ext .ts,.tsx ./src/main/js", 10 | "start": "npm run build && eval $(cat .env) mvn spring-boot:run", 11 | "test": "mvn test", 12 | "watch": "webpack --watch" 13 | }, 14 | "author": "", 15 | "license": "ISC", 16 | "devDependencies": { 17 | "@jitsi/eslint-config": "4.1.5", 18 | "@types/amplitude-js": "^5.11.0", 19 | "@types/react": "16.9.35", 20 | "@types/react-dom": "16.9.8", 21 | "@types/react-router-dom": "5.1.5", 22 | "@typescript-eslint/eslint-plugin": "5.59.5", 23 | "@typescript-eslint/parser": "5.59.5", 24 | "css-loader": "3.5.3", 25 | "eslint": "8.40.0", 26 | "eslint-plugin-import": "2.27.5", 27 | "eslint-plugin-react": "7.32.2", 28 | "sass": "1.26.5", 29 | "sass-loader": "8.0.2", 30 | "style-loader": "1.2.1", 31 | "ts-loader": "7.0.4", 32 | "tsconfig-paths-webpack-plugin": "3.2.0", 33 | "typescript": "3.9.2", 34 | "webpack": "4.43.0", 35 | "webpack-cli": "3.3.11" 36 | }, 37 | "dependencies": { 38 | "@jitsi/logger": "2.0.0", 39 | "amplitude-js": "6.2.0", 40 | "history": "4.10.1", 41 | "i18next": "19.5.2", 42 | "react": "16.13.1", 43 | "react-dom": "16.13.1", 44 | "react-i18next": "11.7.0", 45 | "react-router-dom": "5.2.0", 46 | "react-svg": "11.0.27", 47 | "uuidv4": "6.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.5.1 9 | 10 | 11 | org.jitsi 12 | moderated 13 | 0.0.1 14 | jitsi-moderated-meetings 15 | Jitsi Moderated Meetings 16 | 17 | 18 | 1.8 19 | true 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-web 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-starter-security 30 | 31 | 32 | 33 | org.apache.commons 34 | commons-jcs 35 | 2.2.1 36 | pom 37 | 38 | 39 | 40 | com.auth0 41 | java-jwt 42 | 4.4.0 43 | 44 | 45 | 46 | org.bouncycastle 47 | bcprov-jdk15on 48 | 1.70 49 | 50 | 51 | 52 | com.google.guava 53 | guava 54 | 32.1.2-jre 55 | 56 | 57 | 58 | org.springframework.boot 59 | spring-boot-starter-test 60 | test 61 | 62 | 63 | org.junit.vintage 64 | junit-vintage-engine 65 | 66 | 67 | 68 | 69 | 70 | org.apache.httpcomponents 71 | httpclient 72 | 4.5.12 73 | test 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | org.springframework.boot 82 | spring-boot-maven-plugin 83 | 84 | 85 | 86 | org.apache.maven.plugins 87 | maven-jar-plugin 88 | 3.2.0 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-enforcer-plugin 94 | 3.0.0-M3 95 | 96 | 97 | enforce-java 98 | 99 | enforce 100 | 101 | 102 | 103 | 104 | 1.11 105 | 106 | 107 | 3.6 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-compiler-plugin 117 | 118 | 11 119 | 11 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /public/assets/8x8.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /public/assets/appstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/moderated-meetings/85ae6e82dd5d897d40c938e6c151b27ca00c1781/public/assets/appstore.png -------------------------------------------------------------------------------- /public/assets/bookmark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/fdroid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/moderated-meetings/85ae6e82dd5d897d40c938e6c151b27ca00c1781/public/assets/fdroid.png -------------------------------------------------------------------------------- /public/assets/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/jitsi-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/assets/linkedin.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/assets/playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jitsi/moderated-meetings/85ae6e82dd5d897d40c938e6c151b27ca00c1781/public/assets/playstore.png -------------------------------------------------------------------------------- /public/assets/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Jitsi Moderated Meetings 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | # pre-run.sh is intended to include environment specific 6 | # setup such as env var injection of secrets. 7 | if [ -f /usr/jitsi/pre-run.sh ]; then 8 | . /usr/jitsi/pre-run.sh 9 | fi 10 | 11 | exec java -jar moderated-meetings.jar 12 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | import org.springframework.boot.SpringApplication; 21 | import org.springframework.boot.autoconfigure.SpringBootApplication; 22 | import org.springframework.stereotype.Controller; 23 | import org.springframework.web.bind.annotation.RequestMapping; 24 | 25 | @Controller 26 | @SpringBootApplication 27 | public class Application { 28 | 29 | public static void main(String[] args) { 30 | SpringApplication.run(Application.class, args); 31 | } 32 | 33 | /** 34 | * Maps the JS router paths to index.html. 35 | * 36 | * @return 37 | */ 38 | @RequestMapping("/{id:[a-zA-Z0-9]+}") 39 | public String index() { 40 | return "index.html"; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/Config.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | import org.springframework.boot.json.*; 21 | 22 | import java.net.MalformedURLException; 23 | import java.net.URL; 24 | import java.util.*; 25 | 26 | public abstract class Config { 27 | 28 | public static String getDeploymentUrl() throws MalformedURLException { 29 | return new URL(System.getenv("DEPLOYMENT_URL")).toString(); 30 | } 31 | 32 | public static String getPrivateKeyFileName() { 33 | return System.getenv("PRIVATE_KEY_FILE"); 34 | } 35 | 36 | public static String getPrivateKeyId() { 37 | return System.getenv("PRIVATE_KEY_ID"); 38 | } 39 | 40 | public static int getServerPort() { 41 | String port = System.getenv("PORT"); 42 | return port != null ? Integer.parseInt(port) : 8080; 43 | } 44 | 45 | public static String getTargetTenant() { 46 | return System.getenv("TARGET_TENANT"); 47 | } 48 | 49 | public static String getJwtKeysCacheUrl() { 50 | return System.getenv("JWT_PUB_KEYS_CACHE_URL"); 51 | } 52 | 53 | public static String getTokenAuthUrl() { 54 | return System.getenv("TOKEN_AUTH_URL"); 55 | } 56 | 57 | public static Map getJwtVerify() { 58 | String verify = System.getenv("JWT_VERIFY"); 59 | if (verify != null) { 60 | return JsonParserFactory.getJsonParser().parseMap(System.getenv("JWT_VERIFY")); 61 | } 62 | 63 | return new HashMap<>(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | public abstract class Constants { 21 | // JWT segments 22 | public static final String JWT_AUDIENCE = "jitsi"; 23 | public static final String JWT_CLAIM_CONTEXT = "context"; 24 | public static final String JWT_CLAIM_CONTEXT_GROUP = "group"; 25 | public static final String JWT_CLAIM_USER_ID = "user_id"; 26 | public static final String JWT_CLAIM_ROOM = "room"; 27 | public static final String JWT_CLAIM_NAME = "name"; 28 | public static final String JWT_CLAIM_PICTURE= "picture"; 29 | public static final String JWT_CLAIM_EMAIL = "email"; 30 | public static final String JWT_ISSUER = "jitsi"; 31 | 32 | // URL param for token 33 | public static final String JWT_URL_PARAM_NAME = "jwt"; 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/JwtTokenFilter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | import com.auth0.jwt.*; 21 | import com.auth0.jwt.algorithms.*; 22 | import com.auth0.jwt.exceptions.*; 23 | import com.auth0.jwt.interfaces.*; 24 | import org.jitsi.moderated.jwt.*; 25 | import org.springframework.security.core.context.*; 26 | import org.springframework.stereotype.*; 27 | import org.springframework.web.filter.*; 28 | 29 | import javax.annotation.*; 30 | import javax.servlet.*; 31 | import javax.servlet.http.*; 32 | import java.io.*; 33 | import java.util.*; 34 | 35 | @Component 36 | public class JwtTokenFilter extends OncePerRequestFilter { 37 | private static final JsonKeyProvider keyProvider = new JsonKeyProvider(); 38 | 39 | @Override 40 | protected void doFilterInternal(HttpServletRequest request, 41 | @Nonnull HttpServletResponse response, 42 | @Nonnull FilterChain chain) 43 | throws ServletException, IOException { 44 | String token = request.getParameter("jwt"); 45 | DecodedJWT decodedJWT; 46 | if (token != null) { 47 | try { 48 | Algorithm algorithm = Algorithm.RSA256(keyProvider); 49 | 50 | Map ops = Config.getJwtVerify(); 51 | 52 | Verification verification = JWT.require(algorithm); 53 | 54 | if (ops.get("issuer") != null) { 55 | verification = verification.withIssuer((String)ops.get("issuer")); 56 | } 57 | 58 | if (ops.get("audience") != null) { 59 | verification = verification.withAudience((String)ops.get("audience")); 60 | } 61 | 62 | decodedJWT = verification.build().verify(token); 63 | 64 | if (decodedJWT != null) { 65 | SecurityContextHolder.getContext().setAuthentication(new JwtAuthentication(decodedJWT)); 66 | } 67 | } catch (JWTVerificationException exception){ 68 | // Invalid signature/claims 69 | logger.error("Invalid signature/claims: " 70 | + (exception.getCause() != null ? exception.getCause().getMessage() : exception)); 71 | } 72 | } 73 | 74 | chain.doFilter(request, response); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | import com.fasterxml.jackson.databind.*; 21 | import org.apache.commons.logging.*; 22 | import org.springframework.http.*; 23 | import org.springframework.security.config.annotation.web.builders.*; 24 | import org.springframework.security.config.annotation.web.configuration.*; 25 | import org.springframework.security.config.http.*; 26 | import org.springframework.security.web.authentication.*; 27 | 28 | import javax.servlet.http.*; 29 | import java.net.*; 30 | import java.util.*; 31 | 32 | /** 33 | * Configures the web security if TOKEN_AUTH_URL is set. 34 | */ 35 | @EnableWebSecurity 36 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 37 | private final Log logger = LogFactory.getLog(this.getClass()); 38 | 39 | private final JwtTokenFilter jwtTokenFilter; 40 | 41 | public SecurityConfig(JwtTokenFilter jwtTokenFilter) { 42 | this.jwtTokenFilter = jwtTokenFilter; 43 | } 44 | @Override 45 | protected void configure(HttpSecurity http) throws Exception { 46 | // skip security setting if not configured 47 | if (Config.getTokenAuthUrl() == null) { 48 | return; 49 | } 50 | 51 | // Enable CORS and disable CSRF 52 | http = http.cors().and().csrf().disable(); 53 | 54 | // Set session management to stateless 55 | http = http 56 | .sessionManagement() 57 | .sessionCreationPolicy(SessionCreationPolicy.STATELESS) 58 | .and(); 59 | 60 | // Set unauthorized requests exception handler 61 | http = http 62 | .exceptionHandling() 63 | .authenticationEntryPoint( 64 | (request, response, ex) -> { 65 | try { 66 | String requestPath = new URI(request.getRequestURI()).getPath(); 67 | 68 | if (requestPath.startsWith("/")) { 69 | requestPath = requestPath.substring(1); 70 | } 71 | 72 | String url = Config.getTokenAuthUrl(); 73 | 74 | Map payload = new HashMap<>(); 75 | payload.put("room", requestPath); 76 | 77 | url = url.replace("{state}", new ObjectMapper().writeValueAsString(payload)); 78 | 79 | response.setHeader("Location", new URI(null, url, null).toASCIIString()); 80 | response.setStatus(HttpStatus.FOUND.value()); 81 | 82 | return; 83 | 84 | } catch (URISyntaxException e) { 85 | logger.error(e); 86 | } 87 | 88 | response.sendError( 89 | HttpServletResponse.SC_UNAUTHORIZED, 90 | ex.getMessage() 91 | ); 92 | } 93 | ) 94 | .and(); 95 | 96 | // Set permissions on endpoints 97 | http.authorizeRequests() 98 | // Our public endpoints 99 | .antMatchers("/").permitAll() 100 | .antMatchers("/favicon.ico").permitAll() 101 | .antMatchers("/app.js").permitAll() 102 | .antMatchers("/assets/*").permitAll() 103 | .antMatchers("/rest/rooms").permitAll() 104 | .antMatchers("/rest/config").permitAll() 105 | // Our private endpoints 106 | .anyRequest().authenticated(); 107 | 108 | // Add JWT token filter 109 | http.addFilterBefore(this.jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/ServerConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated; 19 | 20 | import org.springframework.boot.web.server.ConfigurableWebServerFactory; 21 | import org.springframework.boot.web.server.WebServerFactoryCustomizer; 22 | import org.springframework.stereotype.Component; 23 | 24 | @Component 25 | public class ServerConfig implements WebServerFactoryCustomizer { 26 | @Override 27 | public void customize(ConfigurableWebServerFactory factory) { 28 | factory.setPort(Config.getServerPort()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/controller/ModeratedRoomFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.controller; 19 | 20 | import com.auth0.jwt.*; 21 | import com.auth0.jwt.algorithms.Algorithm; 22 | import com.google.common.hash.Hashing; 23 | import org.jitsi.moderated.Config; 24 | import org.jitsi.moderated.Constants; 25 | import org.jitsi.moderated.jwt.*; 26 | import org.jitsi.moderated.model.JoinInfo; 27 | import org.jitsi.moderated.model.ModeratedRoom; 28 | import org.springframework.security.core.*; 29 | import org.springframework.security.core.context.*; 30 | 31 | import java.net.*; 32 | import java.nio.charset.StandardCharsets; 33 | import java.nio.file.Files; 34 | import java.nio.file.Paths; 35 | import java.security.KeyFactory; 36 | import java.security.NoSuchAlgorithmException; 37 | import java.security.interfaces.RSAPrivateKey; 38 | import java.security.spec.PKCS8EncodedKeySpec; 39 | import java.util.HashMap; 40 | import java.util.Map; 41 | import java.util.UUID; 42 | 43 | public class ModeratedRoomFactory { 44 | private static final ModeratedRoomFactory FACTORY = new ModeratedRoomFactory(); 45 | private final Algorithm algorithm; 46 | 47 | private ModeratedRoomFactory() { 48 | this.algorithm = this.initAlgorithm(); 49 | } 50 | 51 | public static ModeratedRoomFactory factory() { 52 | return FACTORY; 53 | } 54 | 55 | public JoinInfo getJoinInfo(ModeratedRoom room) throws NoSuchAlgorithmException, MalformedURLException { 56 | String deployment = Config.getDeploymentUrl(); 57 | String deploymentHost = new URL(deployment).getHost(); 58 | 59 | String roomName = Hashing.sha256().hashString(room.getMeetingId(), StandardCharsets.UTF_8).toString(); 60 | String tenant = Config.getTargetTenant(); 61 | 62 | Map context = new HashMap<>(); 63 | 64 | JWTCreator.Builder builder = JWT.create() 65 | .withIssuer(Constants.JWT_ISSUER) 66 | .withSubject(tenant != null ? tenant : deploymentHost) 67 | .withAudience(Constants.JWT_AUDIENCE) 68 | .withKeyId(Config.getPrivateKeyId()) 69 | .withClaim(Constants.JWT_CLAIM_ROOM, roomName); 70 | 71 | Authentication auth = SecurityContextHolder.getContext().getAuthentication(); 72 | 73 | if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof UserInfo) { 74 | UserInfo info = (UserInfo) auth.getPrincipal(); 75 | 76 | builder 77 | .withClaim(Constants.JWT_CLAIM_USER_ID, info.getUid()) 78 | .withClaim(Constants.JWT_CLAIM_NAME, info.getName()) 79 | .withClaim(Constants.JWT_CLAIM_EMAIL, info.getEmail()) 80 | .withClaim(Constants.JWT_CLAIM_PICTURE, info.getPicture()); 81 | } else { 82 | if (tenant != null) { 83 | context.put(Constants.JWT_CLAIM_CONTEXT_GROUP, tenant); 84 | } 85 | } 86 | 87 | if (!context.isEmpty()) { 88 | builder.withClaim(Constants.JWT_CLAIM_CONTEXT, context); 89 | } 90 | 91 | String token = builder.sign(this.algorithm); 92 | 93 | StringBuilder baseUrl = new StringBuilder(deployment); 94 | 95 | if (!deployment.endsWith("/")) { 96 | baseUrl.append("/"); 97 | } 98 | 99 | if (tenant != null) { 100 | baseUrl.append(tenant).append("/"); 101 | } 102 | 103 | baseUrl.append(roomName); 104 | 105 | return new JoinInfo(roomName, baseUrl.toString(), baseUrl 106 | .append("?") 107 | .append(Constants.JWT_URL_PARAM_NAME) 108 | .append("=") 109 | .append(token) 110 | .toString()); 111 | } 112 | 113 | public ModeratedRoom getModeratedRoom() { 114 | // We use double uuid for aesthetics reasons: to make it 64 byte long, same as the sha hash of the 115 | // join link. 116 | String meetingId = new StringBuilder(UUID.randomUUID().toString()) 117 | .append(UUID.randomUUID().toString()).toString() 118 | .replaceAll("-", ""); 119 | 120 | return new ModeratedRoom(meetingId); 121 | } 122 | 123 | private Algorithm initAlgorithm() { 124 | try { 125 | PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec( 126 | Files.readAllBytes(Paths.get(Config.getPrivateKeyFileName())) 127 | ); 128 | KeyFactory keyFactory = KeyFactory.getInstance("RSA"); 129 | RSAPrivateKey privateKey = (RSAPrivateKey)keyFactory.generatePrivate(spec); 130 | return Algorithm.RSA256(null, privateKey); 131 | } catch (Exception e) { 132 | throw new RuntimeException(e); 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/endpoints/ClientConfigEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.endpoints; 19 | 20 | import org.jitsi.moderated.model.ClientConfig; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.RequestMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | @RestController() 26 | @RequestMapping("/rest/config") 27 | public class ClientConfigEndpoint { 28 | 29 | @GetMapping() 30 | public ClientConfig get() { 31 | return ClientConfig.getInstance(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/endpoints/RoomsEndpoint.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.endpoints; 19 | 20 | import org.jitsi.moderated.controller.ModeratedRoomFactory; 21 | import org.jitsi.moderated.model.JoinInfo; 22 | import org.jitsi.moderated.model.ModeratedRoom; 23 | import org.springframework.web.bind.annotation.GetMapping; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.RestController; 27 | 28 | import java.net.MalformedURLException; 29 | import java.security.NoSuchAlgorithmException; 30 | 31 | @RestController() 32 | @RequestMapping("/rest/rooms") 33 | public class RoomsEndpoint { 34 | 35 | @GetMapping() 36 | public ModeratedRoom getModeratedRoom() { 37 | return ModeratedRoomFactory.factory().getModeratedRoom(); 38 | } 39 | 40 | @GetMapping("{meetingId}") 41 | public JoinInfo getJoinInfo(@PathVariable String meetingId) throws NoSuchAlgorithmException, MalformedURLException { 42 | return ModeratedRoomFactory.factory().getJoinInfo(new ModeratedRoom(meetingId)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/jwt/JsonKeyProvider.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.jwt; 19 | 20 | import com.auth0.jwt.interfaces.*; 21 | import org.apache.commons.logging.*; 22 | import org.jitsi.moderated.*; 23 | import org.springframework.boot.json.*; 24 | import org.springframework.util.*; 25 | 26 | import java.io.*; 27 | import java.net.*; 28 | import java.net.http.*; 29 | import java.nio.charset.*; 30 | import java.security.cert.*; 31 | import java.security.interfaces.*; 32 | import java.time.*; 33 | import java.util.*; 34 | import java.util.concurrent.*; 35 | 36 | /** 37 | * Downloads a list of public files from a URL that will return json file with a mapping between kids and the keys. 38 | * If the response Cache-Control header exists we will respect it and refresh it. 39 | */ 40 | public class JsonKeyProvider implements RSAKeyProvider { 41 | private final Log logger = LogFactory.getLog(this.getClass()); 42 | 43 | private Map keys = new HashMap<>(); 44 | 45 | ScheduledExecutorService updater = Executors.newScheduledThreadPool(1); 46 | 47 | public JsonKeyProvider() { 48 | update(); 49 | } 50 | 51 | private void update() { 52 | try { 53 | String url = Config.getJwtKeysCacheUrl(); 54 | 55 | if (url == null) { 56 | return; 57 | } 58 | 59 | logger.info("Updating keys from: " + url); 60 | 61 | HttpClient client = HttpClient.newBuilder() 62 | .version(HttpClient.Version.HTTP_1_1) 63 | .followRedirects(HttpClient.Redirect.NORMAL) 64 | .build(); 65 | HttpRequest request = HttpRequest.newBuilder() 66 | .uri(URI.create(url)) 67 | .timeout(Duration.ofSeconds(20)) 68 | .header("Content-Type", "application/json") 69 | .build(); 70 | HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString()); 71 | 72 | String cacheHeader = response.headers().firstValue("cache-control").orElse(null); 73 | String maxAge = ""; 74 | if (cacheHeader != null) { 75 | maxAge = Arrays.stream(cacheHeader.split(",")).filter(p -> p.trim().startsWith("max-age")) 76 | .findFirst().orElse(""); 77 | maxAge = maxAge.replace("max-age=", "").trim(); 78 | } 79 | 80 | long updateInSec = -1; 81 | if (StringUtils.hasText(maxAge)) { 82 | updateInSec = Long.parseLong(maxAge); 83 | 84 | // let's schedule new update 60 seconds before the cache expiring 85 | if (updateInSec > 60) { 86 | logger.info("Scheduling update of keys for " + (updateInSec - 60) + " seconds."); 87 | updater.schedule(this::update, updateInSec - 60, TimeUnit.SECONDS); 88 | } 89 | } 90 | 91 | if (response.statusCode() == 200) { 92 | JsonParserFactory.getJsonParser().parseMap(response.body()) 93 | .forEach((k,v) -> { 94 | try { 95 | CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); 96 | InputStream in = new ByteArrayInputStream(((String)v).getBytes(StandardCharsets.UTF_8)); 97 | X509Certificate certificate = (X509Certificate)certFactory.generateCertificate(in); 98 | 99 | keys.put(k, (RSAPublicKey) certificate.getPublicKey()); 100 | } catch (Exception e) { 101 | logger.error("Error parsing public key", e); 102 | } 103 | }); 104 | } 105 | } catch (IOException|InterruptedException e) { 106 | logger.error("Error obtaining public keys", e); 107 | } 108 | } 109 | 110 | @Override 111 | public RSAPublicKey getPublicKeyById(String keyId) { 112 | 113 | return keys.get(keyId); 114 | } 115 | 116 | @Override 117 | public RSAPrivateKey getPrivateKey() { 118 | return null; 119 | } 120 | 121 | @Override 122 | public String getPrivateKeyId() { 123 | return null; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/jwt/JwtAuthentication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.jwt; 19 | 20 | import com.auth0.jwt.interfaces.*; 21 | import org.springframework.security.authentication.*; 22 | 23 | import javax.security.auth.*; 24 | 25 | public class JwtAuthentication extends AbstractAuthenticationToken { 26 | private final Object principal; 27 | 28 | public JwtAuthentication(DecodedJWT jwt) { 29 | super(null); 30 | 31 | // create principal/details from jwt 32 | this.principal = new UserInfo(jwt); 33 | 34 | setAuthenticated(true); 35 | } 36 | 37 | @Override 38 | public Object getCredentials() { 39 | return null; 40 | } 41 | 42 | @Override 43 | public Object getPrincipal() { 44 | return this.principal; 45 | } 46 | 47 | @Override 48 | public boolean implies(Subject subject) { 49 | return super.implies(subject); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/jwt/UserInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.jwt; 19 | 20 | import com.auth0.jwt.interfaces.*; 21 | 22 | /** 23 | * Extracts user info from the token. 24 | */ 25 | public class UserInfo { 26 | private final String uid; 27 | private final String name; 28 | private final String picture; 29 | private final String email; 30 | 31 | public UserInfo(DecodedJWT jwt) { 32 | this.uid = jwt.getClaim("user_id").asString(); 33 | this.name = jwt.getClaim("name").asString(); 34 | this.picture = jwt.getClaim("picture").asString(); 35 | this.email = jwt.getClaim("email").asString(); 36 | } 37 | 38 | public String getUid() { 39 | return uid; 40 | } 41 | 42 | public String getName() { 43 | return name; 44 | } 45 | 46 | public String getPicture() { 47 | return picture; 48 | } 49 | 50 | public String getEmail() { 51 | return email; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/model/ClientConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.model; 19 | 20 | import com.google.common.base.CaseFormat; 21 | 22 | import java.lang.reflect.Field; 23 | import java.lang.reflect.Modifier; 24 | import java.util.logging.Logger; 25 | 26 | public class ClientConfig { 27 | 28 | private final static Logger LOGGER = Logger.getLogger(ClientConfig.class.getName()); 29 | private static ClientConfig INSTANCE; 30 | 31 | // Analytics 32 | private String amplitudeKey; 33 | 34 | // Social 35 | private String fbLink; 36 | private String githubLink; 37 | private String linkedInLink; 38 | private String twitterLink; 39 | 40 | // Stores 41 | private String appStoreLink; 42 | private String fdriodLink; 43 | private String playStoreLink; 44 | 45 | // The authenticate link 46 | private String tokenAuthUrl; 47 | 48 | private ClientConfig() { } 49 | 50 | public String getAmplitudeKey() { 51 | return amplitudeKey; 52 | } 53 | 54 | public String getFbLink() { 55 | return fbLink; 56 | } 57 | 58 | public String getGithubLink() { 59 | return githubLink; 60 | } 61 | 62 | public String getLinkedInLink() { 63 | return linkedInLink; 64 | } 65 | 66 | public String getTwitterLink() { 67 | return twitterLink; 68 | } 69 | 70 | public String getAppStoreLink() { 71 | return appStoreLink; 72 | } 73 | 74 | public String getFdriodLink() { 75 | return fdriodLink; 76 | } 77 | 78 | public String getPlayStoreLink() { 79 | return playStoreLink; 80 | } 81 | 82 | public String getTokenAuthUrl() { 83 | return tokenAuthUrl; 84 | } 85 | 86 | /** 87 | * Initializer method for the singleton client config. This method tries to read all required env vars and assigns 88 | * them to fields in this class by doing proper camel/underscore case conversion (e.g. reads FB_LINK and 89 | * assigns to fbLink). 90 | * 91 | * @return The initiated instance of ClientConfig. 92 | */ 93 | public static ClientConfig getInstance() { 94 | if (INSTANCE == null) { 95 | INSTANCE = new ClientConfig(); 96 | Field[] declaredFields = ClientConfig.class.getDeclaredFields(); 97 | for (Field field: declaredFields) { 98 | if (!Modifier.isStatic(field.getModifiers())) { 99 | String envVarName = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, field.getName()); 100 | String envVarValue = System.getenv(envVarName); 101 | 102 | if (envVarValue != null) { 103 | try { 104 | switch (field.getType().getName()) { 105 | case "java.lang.Integer": 106 | field.set(INSTANCE, Integer.parseInt(envVarValue)); 107 | break; 108 | case "java.lang.Double": 109 | field.set(INSTANCE, Double.parseDouble(envVarValue)); 110 | break; 111 | default: 112 | field.set(INSTANCE, envVarValue); 113 | } 114 | } catch (IllegalAccessException e) { 115 | LOGGER.severe("Error setting client config value: " + envVarName + "->" + field.getName()); 116 | } 117 | } else { 118 | LOGGER.warning("No value for client config " + field.getName() + " (" + envVarName + ")"); 119 | } 120 | } 121 | } 122 | } 123 | 124 | return INSTANCE; 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/model/JoinInfo.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.model; 19 | 20 | public class JoinInfo { 21 | 22 | private final String joinUrl; 23 | private final String moderatorUrl; 24 | private final String roomName; 25 | 26 | public JoinInfo(String roomName, String joinUrl, String moderatorUrl) { 27 | this.roomName = roomName; 28 | this.joinUrl = joinUrl; 29 | this.moderatorUrl = moderatorUrl; 30 | } 31 | 32 | public String getJoinUrl() { 33 | return joinUrl; 34 | } 35 | 36 | public String getModeratorUrl() { 37 | return moderatorUrl; 38 | } 39 | 40 | public String getRoomName() { 41 | return roomName; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/org/jitsi/moderated/model/ModeratedRoom.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Moderated meetings. 3 | * 4 | * Copyright @ 2023 - present 8x8, Inc. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | package org.jitsi.moderated.model; 19 | 20 | public class ModeratedRoom { 21 | private final String meetingId; 22 | 23 | public ModeratedRoom(String meetingId) { 24 | this.meetingId = meetingId; 25 | } 26 | 27 | public String getMeetingId() { 28 | return meetingId; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/js/Application.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from 'history'; 2 | import React, { PureComponent, ReactNode } from 'react'; 3 | import { Route, Router, Switch } from 'react-router-dom'; 4 | import Home from 'screens/Home'; 5 | import Join from 'screens/Join'; 6 | 7 | /** 8 | * The main application class of the app. 9 | */ 10 | export default class Application extends PureComponent { 11 | /** 12 | * Implements PureComponent#render. 13 | * 14 | * @inheritdoc 15 | */ 16 | render(): ReactNode { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/js/components/ConfigContext.tsx: -------------------------------------------------------------------------------- 1 | import Config from 'model/Config'; 2 | import React from 'react'; 3 | 4 | export default React.createContext({} as Config); 5 | -------------------------------------------------------------------------------- /src/main/js/components/Screen.tsx: -------------------------------------------------------------------------------- 1 | import ConfigContext from 'components/ConfigContext'; 2 | import Config from 'model/Config'; 3 | import React, { PureComponent, ReactNode } from 'react'; 4 | import { ReactSVG } from 'react-svg'; 5 | 6 | /** 7 | * Supertype interface for the Props of the component. 8 | */ 9 | export interface Props {} 10 | 11 | 12 | /** 13 | * Supertype interface for the State of the component. 14 | */ 15 | export interface State {} 16 | 17 | /** 18 | * Social links to be rendered in the footer. 19 | */ 20 | const SOCIAL_LINKS = { 21 | facebook: 'https://www.facebook.com/jitsi', 22 | github: 'https://github.com/jitsi', 23 | linkedin: 'https://www.linkedin.com/groups/133669', 24 | twitter: 'https://twitter.com/jitsinews' 25 | }; 26 | 27 | /** 28 | * Abstract class that implements the screen type component that all screens in the app should be derived from. 29 | */ 30 | export default class Screen

extends PureComponent { 31 | static contextType = ConfigContext; 32 | 33 | /** 34 | * Implements PureComponent#render. 35 | * 36 | * @inheritdoc 37 | */ 38 | public render(): ReactNode { 39 | const config = this.context as Config; 40 | 41 | return ( 42 | <> 43 |

44 |
45 | 46 |
47 |
48 |

49 | Jitsi Moderated Meetings 50 |

51 |

52 | Jitsi moderated meetings is a feature that lets you book a meeting 53 | URL in advance where you are the only moderator. 54 |

55 |
56 | { this.renderContent() } 57 |
58 | { /* 59 | Bookmarking doesn't seem to be available on JS level easily, so I comment this part out 60 | but don't waqnt to remove because the styling is done for it. 61 | We can probably do bookmarking through our extension later, or if an API appears for it. 62 | 63 | 74 | */ } 75 |
76 | 171 | 172 | ); 173 | } 174 | 175 | /** 176 | * Function to be implemented by the child class to render the dynamic part of the screen. 177 | */ 178 | renderContent(): ReactNode { 179 | return null; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/main/js/functions/analytics.ts: -------------------------------------------------------------------------------- 1 | import amplitude, { AmplitudeClient } from 'amplitude-js'; 2 | import logger from 'functions/logger'; 3 | import Config from 'model/Config'; 4 | 5 | /** 6 | * Singleton class that encapsulates the interaction with a potential analytics handler. 7 | */ 8 | class Analytics { 9 | amplitudeInstance: AmplitudeClient = amplitude.getInstance(); 10 | analyticsEnabled: boolean = false; 11 | 12 | /** 13 | * Initializes the analytics handler, if it is set up in the passed config. 14 | * 15 | * @param config The congif object downloaded form the backend. 16 | */ 17 | init(config: Config): Promise { 18 | return new Promise(resolve => { 19 | if (config.amplitudeKey) { 20 | // Key is provided, so we init the analytics SDK 21 | this.amplitudeInstance.init(config.amplitudeKey, undefined, undefined, () => { 22 | this.analyticsEnabled = true; 23 | logger.info('Analytics initialized.'); 24 | resolve(); 25 | }); 26 | } else { 27 | logger.info('Analytics are disabled.'); 28 | resolve(); 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * Function to send an analytics event. 35 | * 36 | * @param name The name of the event to send. 37 | * @param meta Optional metadata (object) to be sent with the event. 38 | */ 39 | sendAnalyticsEvent(name: string, meta?: unknown): void { 40 | if (this.analyticsEnabled) { 41 | this.amplitudeInstance.logEvent(name, meta); 42 | } 43 | } 44 | 45 | } 46 | 47 | export default new Analytics(); 48 | -------------------------------------------------------------------------------- /src/main/js/functions/logger.ts: -------------------------------------------------------------------------------- 1 | import Logger from '@jitsi/logger'; 2 | 3 | export default Logger.getLogger('moderated-meetings'); 4 | -------------------------------------------------------------------------------- /src/main/js/functions/restUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Wrapper to fetch URLs with common logic. May be extended later. 3 | * 4 | * @param url The URL to fetch. 5 | */ 6 | export async function get(url: string): Promise { 7 | return await (await fetch(url)).json(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/js/functions/urlUtils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Function to parse query params of a page. 3 | * 4 | * @param query The query string returned by location.search 5 | * (See: {@link https://reacttraining.com/react-router/web/api/location}). 6 | */ 7 | export function parseQueryParams(query: string = ''): Record { 8 | const parsedQueryParams = {}; 9 | const params = query.substr(query.startsWith('?') ? 1 : 0).split('&'); 10 | 11 | for (const param of params) { 12 | const [ key, value ] = param.split('='); 13 | 14 | if (key) { 15 | parsedQueryParams[key] = value; 16 | } 17 | } 18 | 19 | return parsedQueryParams; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/js/index.tsx: -------------------------------------------------------------------------------- 1 | import '../scss/index.scss'; 2 | 3 | import Application from 'Application'; 4 | import ConfigContext from 'components/ConfigContext'; 5 | import analytics from 'functions/analytics'; 6 | import { get } from 'functions/restUtils'; 7 | import Config from 'model/Config'; 8 | import React from 'react'; 9 | import ReactDOM from 'react-dom'; 10 | 11 | get('/rest/config').then((config: Config) => { 12 | analytics.init(config).then(() => { 13 | ReactDOM.render( 14 | 15 | , document.getElementById('jitsi-moderated-meetings')); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/main/js/model/Config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Interface to store the config that was downloaded from the server. 3 | */ 4 | interface Config { 5 | amplitudeKey?: string; 6 | fbLink?: string; 7 | githubLink?: string; 8 | linkedInLink?: string; 9 | twitterLink?: string; 10 | appStoreLink?: string; 11 | fdriodLink?: string; 12 | playStoreLink?: string; 13 | tokenAuthUrl?: string; 14 | } 15 | 16 | export default Config; 17 | -------------------------------------------------------------------------------- /src/main/js/screens/Home.tsx: -------------------------------------------------------------------------------- 1 | import Screen, { Props as AbstractProps, State as AbstractState } from 'components/Screen'; 2 | import analytics from 'functions/analytics'; 3 | import { parseQueryParams } from 'functions/urlUtils'; 4 | import React, { ReactNode } from 'react'; 5 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 6 | 7 | /** 8 | * Type interface for the possible query params of the page. 9 | */ 10 | interface QueryParams { 11 | 12 | /** 13 | * If true, the app auto generates the links, so the user doesn't have to 14 | * click the button. 15 | */ 16 | autoGenerate?: string; 17 | } 18 | 19 | /** 20 | * Type interface for the Props of the component. 21 | */ 22 | interface Props extends AbstractProps, RouteComponentProps {} 23 | 24 | /** 25 | * Type interface for the State of the component. 26 | */ 27 | interface State extends AbstractState { 28 | 29 | /** 30 | * The generated join URL, if any. 31 | */ 32 | joinUrl?: string; 33 | 34 | /** 35 | * The generated moderator join URL, if any. 36 | */ 37 | moderatorUrl?: string; 38 | 39 | /** 40 | * True of the generate button should be rendered, false otherwise. 41 | */ 42 | showHome: boolean; 43 | } 44 | 45 | /** 46 | * Screen that implements the home page of the app. 47 | */ 48 | class Home extends Screen { 49 | /** 50 | * Query params of the page. 51 | */ 52 | params: QueryParams; 53 | 54 | /** 55 | * Instantiates a new component. 56 | * 57 | * @inheritdoc 58 | */ 59 | constructor(props: Props) { 60 | super(props); 61 | 62 | this.params = parseQueryParams(this.props.location.search) as QueryParams; 63 | 64 | this.state = { 65 | showHome: !this.params.hasOwnProperty('autoGenerate') 66 | }; 67 | 68 | this.onGenerateMeeting = this.onGenerateMeeting.bind(this); 69 | } 70 | 71 | /** 72 | * Implements PureComponent#componentDidMount. 73 | * 74 | * @inheritdoc 75 | */ 76 | public componentDidMount(): void { 77 | if (this.params.hasOwnProperty('autoGenerate')) { 78 | this.onGenerateMeeting(); 79 | } 80 | 81 | analytics.sendAnalyticsEvent('screen:home'); 82 | } 83 | 84 | /** 85 | * Implements Screen#renderContent. 86 | * 87 | * @inheritdoc 88 | */ 89 | public renderContent(): ReactNode { 90 | if (!this.state.showHome) { 91 | return null; 92 | } 93 | 94 | return ( 95 |
96 | 101 |
102 | ); 103 | } 104 | 105 | /** 106 | * Function to generate a moderated meeting. This can either be 107 | * run by clicking the generate button on the page or automatically 108 | * with the 'autoGenerate' query param. 109 | */ 110 | private async onGenerateMeeting(): Promise { 111 | const response = await (await fetch('/rest/rooms')).json(); 112 | 113 | this.props.history.push(`/${response.meetingId}`); 114 | 115 | analytics.sendAnalyticsEvent('action:generate-meeting'); 116 | } 117 | 118 | } 119 | 120 | export default withRouter(Home); 121 | -------------------------------------------------------------------------------- /src/main/js/screens/Join.tsx: -------------------------------------------------------------------------------- 1 | import Screen, { Props as AbstractProps, State as AbstractState } from 'components/Screen'; 2 | import analytics from 'functions/analytics'; 3 | import React, { ReactNode } from 'react'; 4 | import { RouteComponentProps, withRouter } from 'react-router-dom'; 5 | import { ReactSVG } from 'react-svg'; 6 | 7 | import Config from '../model/Config'; 8 | 9 | /** 10 | * Type interface for the URL params of the page. 11 | */ 12 | interface URLParams { 13 | meetingId: string; 14 | } 15 | 16 | /** 17 | * Type interface for the Props of the component. 18 | */ 19 | interface Props extends AbstractProps, RouteComponentProps {} 20 | 21 | /** 22 | * Type interface for the State of the component. 23 | */ 24 | interface State extends AbstractState { 25 | joinUrl?: string | undefined; 26 | moderatorUrl?: string | undefined; 27 | } 28 | 29 | /** 30 | * Screen that implements the join page of the app. 31 | */ 32 | class Join extends Screen { 33 | /** 34 | * Instantiates a new component. 35 | * 36 | * @inheritdoc 37 | */ 38 | constructor(props: Props) { 39 | super(props); 40 | 41 | this.state = {}; 42 | 43 | this.joinAsModerator = this.joinAsModerator.bind(this); 44 | } 45 | 46 | /** 47 | * Implements PureComponent#componentDidMount. 48 | * 49 | * @inheritdoc 50 | */ 51 | async componentDidMount(): Promise { 52 | const { meetingId } = this.props.match.params; 53 | const config = this.context as Config; 54 | const token = new URLSearchParams(window.location.search).get('jwt'); 55 | let extraParams = ''; 56 | 57 | if (token) { 58 | extraParams = `?jwt=${token}`; 59 | } 60 | 61 | const fetchOptions = {}; 62 | 63 | if (config.tokenAuthUrl) { 64 | // we want to disable redirect and manually handle that 65 | // @ts-ignore 66 | fetchOptions.redirect = 'manual'; 67 | } 68 | 69 | const roomsResponse = await fetch(`/rest/rooms/${meetingId}${extraParams}`, fetchOptions); 70 | 71 | // status 0 is special status when redirected in manual mode 72 | if (roomsResponse.status === 0 && config.tokenAuthUrl) { 73 | let tokenUrl = config.tokenAuthUrl; 74 | 75 | tokenUrl = tokenUrl.replace('{state}', encodeURIComponent(JSON.stringify({ 76 | room: meetingId 77 | }))); 78 | 79 | window.location.href = tokenUrl; 80 | 81 | return; 82 | } 83 | 84 | const { joinUrl, moderatorUrl, error, status } = await roomsResponse.json(); 85 | 86 | if (config.tokenAuthUrl) { 87 | if (error && status === 401) { 88 | let tokenUrl = config.tokenAuthUrl; 89 | 90 | tokenUrl = tokenUrl.replace('{state}', encodeURIComponent(JSON.stringify({ 91 | room: meetingId 92 | }))); 93 | 94 | window.location.href = tokenUrl; 95 | } else if (!error) { 96 | // let's rewrite url and hide the jwt 97 | history.replaceState( 98 | history.state, 99 | document?.title || '', 100 | location.pathname); 101 | } 102 | } 103 | 104 | this.setState({ 105 | joinUrl, 106 | moderatorUrl 107 | }); 108 | 109 | analytics.sendAnalyticsEvent('screen:join'); 110 | } 111 | 112 | /** 113 | * Implements Screen#renderContent. 114 | * 115 | * @inheritdoc 116 | */ 117 | public renderContent(): ReactNode { 118 | const { joinUrl } = this.state; 119 | 120 | if (!joinUrl) { 121 | return null; 122 | } 123 | 124 | return ( 125 | <> 126 |
127 | 130 |
131 | 136 | 145 |
146 |
147 |
148 |
149 | 152 |
153 | 158 | 167 |
168 | 173 |
174 | 175 | ); 176 | } 177 | 178 | /** 179 | * Callback to be invoked on clicking one of the copy buttons. 180 | * 181 | * @param fieldId The field ID to copy content from. 182 | */ 183 | private copyUrl(fieldId: string): () => void { 184 | return (): void => { 185 | const copyText = document.getElementById(fieldId) as HTMLInputElement; 186 | 187 | copyText.select(); 188 | 189 | // Workaround for mobile devices 190 | copyText.setSelectionRange(0, 99999); 191 | document.execCommand('copy'); 192 | copyText.setSelectionRange(0, 0); 193 | copyText.blur(); 194 | 195 | analytics.sendAnalyticsEvent('action:copy-url', { field: fieldId }); 196 | }; 197 | } 198 | 199 | /** 200 | * Callback to be invoked when the user clicks the join as moderator button. 201 | */ 202 | private joinAsModerator(): void { 203 | const { moderatorUrl } = this.state; 204 | 205 | if (moderatorUrl) { 206 | document.location.href = moderatorUrl; 207 | } 208 | 209 | analytics.sendAnalyticsEvent('action:join-moderator'); 210 | } 211 | 212 | } 213 | 214 | export default withRouter(Join); 215 | -------------------------------------------------------------------------------- /src/main/js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./build", /* Redirect output structure to the directory. */ 16 | "rootDir": ".", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | "noImplicitAny": false, 28 | /* Raise error on expressions and declarations with an implied 'any' type. */ 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | "baseUrl": ".", 45 | /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | "rootDirs": [], 48 | /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true 53 | /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 54 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 55 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 56 | 57 | /* Source Map Options */ 58 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 59 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 60 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 61 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 62 | 63 | /* Experimental Options */ 64 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 65 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/js/typings/Logger.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jitsi-meet-logger' { 2 | 3 | /** 4 | * Class replresents a logger in any JS/TS jitsi applications. 5 | */ 6 | class Logger { 7 | info(message: string, ...params): void; 8 | } 9 | 10 | export function getLogger(id: string, transports?: Array | undefined, options?: unknown): Logger; 11 | } 12 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/main/scss/index.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | 3 | @import 'reset.css'; 4 | @import 'ui-kit.scss'; 5 | @import 'variables.scss'; 6 | 7 | #jitsi-moderated-meetings { 8 | align-items: stretch; 9 | color: $text; 10 | flex: 1; 11 | 12 | #top-gradient { 13 | background: $gradient; 14 | background: linear-gradient(180deg, $gradient 0%, color.change($gradient, $alpha: 0) 100%); 15 | height: 340px; 16 | position: absolute; 17 | top: 0; 18 | width: 100%; 19 | z-index: -1; 20 | } 21 | 22 | #content-box { 23 | align-items: stretch; 24 | border: 1px solid rgba(255, 255, 255, .4); 25 | border-radius: 4px; 26 | margin: 24px 0; 27 | 28 | .content-box-section { 29 | padding: 24px; 30 | } 31 | 32 | hr { 33 | margin: 0; 34 | } 35 | } 36 | 37 | #bookmark-note { 38 | margin: 3px 0; 39 | 40 | #bookmark-icon { 41 | margin: 0 13px; 42 | } 43 | } 44 | 45 | #footer-wrapper { 46 | max-width: 480px; 47 | padding: 40px 0; 48 | 49 | .line-wrapper { 50 | align-items: center; 51 | flex-direction: row; 52 | 53 | img { 54 | height: 100%; 55 | object-fit: cover; 56 | width: 100%; 57 | } 58 | 59 | >span, 60 | >p { 61 | font-size: .75em; 62 | } 63 | 64 | p { 65 | flex: 1; 66 | } 67 | 68 | &#copyright { 69 | justify-content: space-between; 70 | 71 | >div { 72 | margin: 0 8px; 73 | } 74 | } 75 | 76 | &#mobile-wrapper { 77 | margin-right: 10px; 78 | } 79 | 80 | #mobile-links { 81 | align-items: stretch; 82 | flex-direction: column; 83 | 84 | > a { 85 | margin: 5px 0; 86 | 87 | > img { 88 | width: 100%; 89 | } 90 | } 91 | } 92 | 93 | #legal-links { 94 | >a { 95 | color: $text; 96 | margin-right: 15px; 97 | text-decoration: none; 98 | } 99 | } 100 | 101 | #social-wrapper { 102 | flex-direction: row; 103 | 104 | >a { 105 | cursor: pointer; 106 | margin: 0 12px; 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | @media (max-width: 480px) { 114 | #jitsi-moderated-meetings { 115 | #footer-wrapper { 116 | padding: 20px 0; 117 | 118 | .line-wrapper { 119 | flex-direction: column; 120 | 121 | > * { 122 | margin: 5px 0; 123 | } 124 | 125 | #mobile-links { 126 | flex-direction: row; 127 | } 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /src/main/scss/reset.css: -------------------------------------------------------------------------------- 1 | /* http://meyerweb.com/eric/tools/css/reset/ 2 | v2.0 | 20110126 3 | License: none (public domain) 4 | */ 5 | 6 | html, body, div, span, applet, object, iframe, 7 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 8 | a, abbr, acronym, address, big, cite, code, 9 | del, dfn, em, img, ins, kbd, q, s, samp, 10 | small, strike, strong, sub, sup, tt, var, 11 | b, u, i, center, 12 | dl, dt, dd, ol, ul, li, 13 | fieldset, form, label, legend, 14 | table, caption, tbody, tfoot, thead, tr, th, td, 15 | article, aside, canvas, details, embed, 16 | figure, figcaption, footer, header, hgroup, 17 | menu, nav, output, ruby, section, summary, 18 | time, mark, audio, video { 19 | margin: 0; 20 | padding: 0; 21 | border: 0; 22 | font-size: 100%; 23 | font: inherit; 24 | vertical-align: baseline; 25 | } 26 | /* HTML5 display-role reset for older browsers */ 27 | article, aside, details, figcaption, figure, 28 | footer, header, hgroup, menu, nav, section { 29 | display: block; 30 | } 31 | body { 32 | line-height: 1; 33 | } 34 | ol, ul { 35 | list-style: none; 36 | } 37 | blockquote, q { 38 | quotes: none; 39 | } 40 | blockquote:before, blockquote:after, 41 | q:before, q:after { 42 | content: ''; 43 | content: none; 44 | } 45 | table { 46 | border-collapse: collapse; 47 | border-spacing: 0; 48 | } -------------------------------------------------------------------------------- /src/main/scss/ui-kit.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | body { 8 | background: $background; 9 | display: flex; 10 | font-family: 'Open Sans', sans-serif; 11 | font-size: $base-font-size; 12 | font-weight: 300; 13 | letter-spacing: -0.006em; 14 | min-height: 100vh; 15 | } 16 | 17 | button { 18 | border: none; 19 | color: $text; 20 | cursor: pointer; 21 | display: flex; 22 | flex-direction: row; 23 | font-size: .9em; 24 | justify-content: center; 25 | 26 | &.primary { 27 | align-self: stretch; 28 | background-color: $button-primary; 29 | border-radius: 4px; 30 | padding: 11px; 31 | } 32 | 33 | &.text { 34 | align-self: center; 35 | background: none; 36 | } 37 | } 38 | 39 | div, 40 | footer, 41 | header, 42 | main { 43 | display: flex; 44 | flex-direction: column; 45 | } 46 | 47 | footer { 48 | align-items: center; 49 | background: $footer; 50 | } 51 | 52 | footer, 53 | header, 54 | main { 55 | padding: 10px; 56 | } 57 | 58 | h1 { 59 | font-size: 2.2em; 60 | font-weight: 400; 61 | line-height: 3em; 62 | } 63 | 64 | header { 65 | padding: 32px 40px; 66 | } 67 | 68 | hr { 69 | border: none; 70 | border-top: 1px solid rgba(255, 255, 255, 0.2); 71 | margin: 20px 0; 72 | width: 100%; 73 | } 74 | 75 | input { 76 | flex: 1; 77 | font-family: 'Open Sans', sans-serif; 78 | } 79 | 80 | label { 81 | font-size: .9em; 82 | } 83 | 84 | main { 85 | align-items: stretch; 86 | align-self: center; 87 | flex: 1; 88 | flex-direction: column; 89 | max-width: 480px; 90 | } 91 | 92 | p { 93 | font-size: 1em; 94 | line-height: 1.5em; 95 | } 96 | 97 | .centered { 98 | text-align: center; 99 | } 100 | 101 | .copy-field-wrapper { 102 | background: rgba(255, 255, 255, .2); 103 | border-radius: 4px; 104 | flex-direction: row; 105 | margin: 8px 0; 106 | padding: 8px; 107 | 108 | input { 109 | background: none; 110 | border: none; 111 | color: $text; 112 | font-size: 1em; 113 | font-weight: 300; 114 | pointer-events: none; 115 | text-overflow: ellipsis; 116 | } 117 | } 118 | 119 | .store-badge-wrapper { 120 | align-items: center; 121 | display: flex; 122 | height: 40px; 123 | justify-content: center; 124 | margin: 0 5px; 125 | width: 135px; 126 | } 127 | 128 | @media (max-width: 480px) { 129 | h1 { 130 | font-size: 1.8em; 131 | line-height: 2em; 132 | } 133 | 134 | header { 135 | padding: 16px 15px; 136 | } 137 | } -------------------------------------------------------------------------------- /src/main/scss/variables.scss: -------------------------------------------------------------------------------- 1 | // Colors 2 | 3 | $background: #284389; 4 | $base-font-size: 16px; 5 | $button-primary: #0072DB; 6 | $footer: #191D29; 7 | $gradient: rgba(26, 45, 94, 1); 8 | $text: white; -------------------------------------------------------------------------------- /src/test/java/org/jitsi/moderated/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.jitsi.moderated; 2 | 3 | import com.auth0.jwt.JWT; 4 | import com.auth0.jwt.interfaces.DecodedJWT; 5 | import org.apache.http.NameValuePair; 6 | import org.apache.http.client.utils.URLEncodedUtils; 7 | import org.jitsi.moderated.controller.ModeratedRoomFactory; 8 | import org.jitsi.moderated.model.JoinInfo; 9 | import org.jitsi.moderated.model.ModeratedRoom; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | 14 | import java.net.MalformedURLException; 15 | import java.net.URI; 16 | import java.net.URISyntaxException; 17 | import java.net.URL; 18 | import java.nio.charset.Charset; 19 | import java.security.NoSuchAlgorithmException; 20 | import java.util.Iterator; 21 | import java.util.List; 22 | import java.util.concurrent.atomic.AtomicReference; 23 | 24 | @SpringBootTest 25 | class ApplicationTests { 26 | 27 | /** 28 | * Test to validate the format of the generated URLs. 29 | * 30 | * @throws NoSuchAlgorithmException Thrown if JWT token generation fails. 31 | * @throws MalformedURLException Thrown if any of the generated URLs are malformed. 32 | */ 33 | @Test 34 | void generatedDataFormat() throws NoSuchAlgorithmException, MalformedURLException { 35 | final ModeratedRoomFactory factory = ModeratedRoomFactory.factory(); 36 | final ModeratedRoom moderatedRoom = factory.getModeratedRoom(); 37 | final JoinInfo joinInfo = factory.getJoinInfo(moderatedRoom); 38 | 39 | // These URLs should pass common tests, so creating an array of it 40 | final String[] urls = { 41 | joinInfo.getJoinUrl(), 42 | joinInfo.getModeratorUrl() 43 | }; 44 | 45 | for (String url: urls) { 46 | // URLs must be valid 47 | AtomicReference u = new AtomicReference<>(); 48 | 49 | Assertions.assertDoesNotThrow(() -> { 50 | u.set(new URL(url)); 51 | }); 52 | 53 | String path = u.get().getPath(); 54 | 55 | // A common mistake is to have double // after the host:port 56 | Assertions.assertFalse(path.startsWith("//")); 57 | 58 | // Path must have a fixed format 59 | Assertions.assertTrue(path.matches(new StringBuilder("/") 60 | .append(Config.getTargetTenant()) 61 | .append("/") 62 | .append(joinInfo.getRoomName()) 63 | .toString())); 64 | 65 | // Both URLs should start with the deployment URL 66 | Assertions.assertTrue(url.startsWith(Config.getDeploymentUrl())); 67 | } 68 | 69 | // We expect the room name and the moderated link room name segment to be of equal size 70 | Assertions.assertEquals(moderatedRoom.getMeetingId().length(), joinInfo.getRoomName().length()); 71 | } 72 | 73 | /** 74 | * Test to validate that the generated JWT contains all the right information. 75 | * 76 | * @throws NoSuchAlgorithmException Thrown if JWT token generation fails. 77 | * @throws URISyntaxException Thrown if any of the generated URLs are malformed. 78 | * @throws MalformedURLException Thrown if any of the generated URLs are malformed. 79 | */ 80 | @Test 81 | void jwtFormat() throws NoSuchAlgorithmException, URISyntaxException, MalformedURLException { 82 | final ModeratedRoomFactory factory = ModeratedRoomFactory.factory(); 83 | final ModeratedRoom moderatedRoom = factory.getModeratedRoom(); 84 | final JoinInfo joinInfo = factory.getJoinInfo(moderatedRoom); 85 | final URI moderatorURI = new URI(joinInfo.getModeratorUrl()); 86 | String jwt = null; 87 | 88 | List params = URLEncodedUtils.parse(moderatorURI, Charset.defaultCharset()); 89 | 90 | Iterator iterator = params.iterator(); 91 | while(iterator.hasNext() && jwt == null) { 92 | final NameValuePair param = iterator.next(); 93 | 94 | if ("jwt".equals(param.getName())) { 95 | jwt = param.getValue(); 96 | } 97 | } 98 | 99 | // We have to have a JWT token in the moderator join URL 100 | Assertions.assertNotNull(jwt); 101 | 102 | // The token must contain all required fields 103 | DecodedJWT decodedJWT = JWT.decode(jwt); 104 | Assertions.assertEquals(decodedJWT.getIssuer(), Constants.JWT_ISSUER); 105 | Assertions.assertEquals(decodedJWT.getSubject(), Config.getTargetTenant()); 106 | Assertions.assertEquals(decodedJWT.getAudience().get(0), Constants.JWT_AUDIENCE); 107 | Assertions.assertEquals(decodedJWT.getKeyId(), Config.getPrivateKeyId()); 108 | Assertions.assertEquals(joinInfo.getRoomName(), decodedJWT.getClaim(Constants.JWT_CLAIM_ROOM).asString()); 109 | Assertions.assertEquals(Config.getTargetTenant(), decodedJWT.getClaim(Constants.JWT_CLAIM_CONTEXT).asMap().get(Constants.JWT_CLAIM_CONTEXT_GROUP)); 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cd "$(dirname "$0")" 3 | 4 | npm start 5 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const path = require('path'); 3 | const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); 4 | 5 | module.exports = { 6 | entry: { 7 | 'app': './src/main/js/index.tsx' 8 | }, 9 | mode: 'development', 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.ts(x?)$/, 14 | exclude: /node_modules/, 15 | use: [ 16 | { 17 | loader: 'ts-loader' 18 | } 19 | ] 20 | }, 21 | { 22 | test: /\.css$/i, 23 | use: [ 'style-loader', 'css-loader' ] 24 | }, 25 | { 26 | test: /\.s[ac]ss$/i, 27 | use: [ 28 | 'style-loader', 29 | 'css-loader', 30 | 'sass-loader' 31 | ] 32 | } 33 | ] 34 | }, 35 | output: { 36 | path: path.join(__dirname, 'public') 37 | }, 38 | resolve: { 39 | extensions: [ '.js', '.ts', '.tsx', '.jsx' ], 40 | plugins: [ new TsconfigPathsPlugin({ configFile: './src/main/js/tsconfig.json' }) ] 41 | } 42 | }; 43 | --------------------------------------------------------------------------------