diff --git a/openvidu-sample-app/.gitignore b/openvidu-sample-app/.gitignore new file mode 100644 index 00000000..a37e8ea7 --- /dev/null +++ b/openvidu-sample-app/.gitignore @@ -0,0 +1,255 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +backend/openvidu-sample-app/target/* +backend/openvidu-sample-app/src/main/resources/static/* + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/openvidu-sample-app/BuildFrontWarAndRun.sh b/openvidu-sample-app/BuildFrontWarAndRun.sh new file mode 100755 index 00000000..8a2e64af --- /dev/null +++ b/openvidu-sample-app/BuildFrontWarAndRun.sh @@ -0,0 +1,6 @@ +#!/bin/sh +cd frontend +ng build --output-path ./../backend/openvidu-sample-app/src/main/resources/static +cd ../backend/openvidu-sample-app +mvn clean package +java -jar target/openvidu-sample-app-0.0.1-SNAPSHOT.war diff --git a/openvidu-sample-app/LICENSE b/openvidu-sample-app/LICENSE new file mode 100644 index 00000000..8dada3ed --- /dev/null +++ b/openvidu-sample-app/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/openvidu-sample-app/backend/openvidu-sample-app/.gitignore b/openvidu-sample-app/backend/openvidu-sample-app/.gitignore new file mode 100644 index 00000000..b83d2226 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/.gitignore @@ -0,0 +1 @@ +/target/ diff --git a/openvidu-sample-app/backend/openvidu-sample-app/pom.xml b/openvidu-sample-app/backend/openvidu-sample-app/pom.xml new file mode 100644 index 00000000..c059c918 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/pom.xml @@ -0,0 +1,126 @@ + + 4.0.0 + + openvidu + openvidu-sample-app + 0.0.1-SNAPSHOT + war + + openvidu-sample-app + http://maven.apache.org + + + org.springframework.boot + spring-boot-starter-parent + 1.4.1.RELEASE + + + + UTF-8 + 1.8 + openvidu.openvidu_sample_app.App + + + + + ${project.artifactId}-${project.version} + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.springframework + springloaded + 1.2.6.RELEASE + + + + + + + org.codehaus.mojo + exec-maven-plugin + + ${start-class} + + + + + maven-war-plugin + + + + src/main/ebextensions + .ebextensions + true + + + + + + + + + + + + junit + junit + test + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-devtools + + + + mysql + mysql-connector-java + + + + commons-validator + commons-validator + 1.5.1 + + + + org.springframework.boot + spring-boot-starter-websocket + + + + com.googlecode.json-simple + json-simple + + + + org.apache.httpcomponents + httpclient + 4.5 + + + + + diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/ebextensions/configuration.config b/openvidu-sample-app/backend/openvidu-sample-app/src/main/ebextensions/configuration.config new file mode 100644 index 00000000..e69de29b diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/App.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/App.java new file mode 100644 index 00000000..c49bf1ea --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/App.java @@ -0,0 +1,30 @@ +package openvidu.openvidu_sample_app; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; +import org.springframework.web.socket.config.annotation.EnableWebSocket; + +@SpringBootApplication +@EnableWebSocket +public class App +{ + public static void main( String[] args ) + { + SpringApplication.run(App.class, args); + } + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurerAdapter() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**").allowedOrigins("*"); + } + }; + } + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/DatabaseInitializer.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/DatabaseInitializer.java new file mode 100644 index 00000000..33660b0b --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/DatabaseInitializer.java @@ -0,0 +1,50 @@ +package openvidu.openvidu_sample_app; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Controller; + +import openvidu.openvidu_sample_app.user.UserRepository; +import openvidu.openvidu_sample_app.user.User; +import openvidu.openvidu_sample_app.lesson.LessonRepository; +import openvidu.openvidu_sample_app.lesson.Lesson; + +@Controller +public class DatabaseInitializer implements CommandLineRunner { + + @Autowired + private UserRepository userRepository; + + @Autowired + private LessonRepository lessonRepository; + + @Override + public void run(String... args) throws Exception { + + //Sample users + User user1 = new User("student1@gmail.com", "pass", "Student Imprudent", "ROLE_STUDENT"); + User user2 = new User("student2@gmail.com", "pass", "Student Concludent", "ROLE_STUDENT"); + User user3 = new User("teacher@gmail.com", "pass", "Teacher Cheater", "ROLE_TEACHER"); + + //Saving users + userRepository.save(user1); + userRepository.save(user2); + userRepository.save(user3); + + //Sample lessons + Lesson c1 = new Lesson("Lesson number 1", user3); + Lesson c2 = new Lesson("Lesson number 2", user3); + + c1.getAttenders().add(user1); + c1.getAttenders().add(user2); + c1.getAttenders().add(user3); + + c2.getAttenders().add(user1); + c2.getAttenders().add(user3); + + //Saving lessons + lessonRepository.save(c1); + lessonRepository.save(c2); + } + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/Lesson.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/Lesson.java new file mode 100644 index 00000000..0dac94ad --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/Lesson.java @@ -0,0 +1,84 @@ +package openvidu.openvidu_sample_app.lesson; + +import java.util.Set; +import java.util.HashSet; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; + +import com.fasterxml.jackson.annotation.JsonView; +import openvidu.openvidu_sample_app.user.User; + +@Entity +public class Lesson { + + public interface SimpleCourseList {} + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @JsonView(SimpleCourseList.class) + private long id; + + @JsonView(SimpleCourseList.class) + private String title; + + @ManyToOne + private User teacher; + + @ManyToMany + private Set attenders; + + public Lesson() {} + + public Lesson(String title, User teacher) { + this.title = title; + this.teacher = teacher; + this.attenders = new HashSet<>(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public User getTeacher() { + return teacher; + } + + public void setTeacher(User teacher) { + this.teacher = teacher; + } + + public Set getAttenders() { + return attenders; + } + + public void setAttenders(Set attenders) { + this.attenders = attenders; + } + + //To make 'user.getLesson().remove(lesson)' possible + @Override + public boolean equals(Object other){ + if (other == null) return false; + if (other == this) return true; + if (!(other instanceof Lesson))return false; + Lesson otherCourse = (Lesson)other; + return (otherCourse.id == this.id); + } +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonController.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonController.java new file mode 100644 index 00000000..240cb944 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonController.java @@ -0,0 +1,299 @@ +package openvidu.openvidu_sample_app.lesson; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import org.apache.commons.validator.routines.EmailValidator; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import openvidu.openvidu_sample_app.user.User; +import openvidu.openvidu_sample_app.user.UserComponent; +import openvidu.openvidu_sample_app.user.UserRepository; + +@RestController +@RequestMapping("/api-lessons") +public class LessonController { + + @Autowired + private LessonRepository lessonRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserComponent user; + + private class AddAttendersResponse { + public Collection attendersAdded; + public Collection attendersAlreadyAdded; + public Collection emailsInvalid; + public Collection emailsValidNotRegistered; + } + + @RequestMapping(value = "/user/{id}", method = RequestMethod.GET) + public ResponseEntity> getLessons(@PathVariable(value="id") String id){ + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + long id_i = -1; + try{ + id_i = Long.parseLong(id); + }catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + Set s = new HashSet<>(); + s.add(id_i); + Collection users = userRepository.findAll(s); + Collection lessons = new HashSet<>(); + lessons = lessonRepository.findByAttenders(users); + return new ResponseEntity<>(lessons ,HttpStatus.OK); + } + + + + @RequestMapping(value = "/lesson/{id}", method = RequestMethod.GET) + public ResponseEntity getLesson(@PathVariable(value="id") String id){ + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + long id_i = -1; + try{ + id_i = Long.parseLong(id); + }catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + Lesson lesson = lessonRepository.findOne(id_i); + return new ResponseEntity<>(lesson ,HttpStatus.OK); + } + + + + @RequestMapping(value = "/new", method = RequestMethod.POST) + public ResponseEntity newLesson(@RequestBody Lesson lesson) { + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + User userLogged = user.getLoggedUser(); + + if (!user.hasRoleTeacher()){ + // Students are not authorized to add new lessons + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + // Updating lesson ('teacher', 'attenders') + lesson.setTeacher(userLogged); + lesson.getAttenders().add(userLogged); + + // Saving lesson + lessonRepository.save(lesson); + lessonRepository.flush(); + + lesson = lessonRepository.findOne(lesson.getId()); + return new ResponseEntity<>(lesson, HttpStatus.CREATED); + } + + + + @RequestMapping(value = "/edit", method = RequestMethod.PUT) + public ResponseEntity modifyLesson(@RequestBody Lesson lesson) { + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + Lesson c = lessonRepository.findOne(lesson.getId()); + + checkAuthorization(c, c.getTeacher()); + + //Modifying the lesson attributes + c.setTitle(lesson.getTitle()); + + //Saving the modified lesson + lessonRepository.save(c); + return new ResponseEntity<>(c, HttpStatus.OK); + } + + + + @RequestMapping(value = "/delete/{lessonId}", method = RequestMethod.DELETE) + public ResponseEntity deleteLesson(@PathVariable(value="lessonId") String lessonId) { + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + long id_lesson = -1; + try{ + id_lesson = Long.parseLong(lessonId); + }catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + Lesson c = lessonRepository.findOne(id_lesson); + + checkAuthorization(c, c.getTeacher()); + + //Removing the deleted lesson from its attenders + Collection lessons = new HashSet<>(); + lessons.add(c); + Collection users = userRepository.findByLessons(lessons); + for(User u: users){ + u.getLessons().remove(c); + } + userRepository.save(users); + c.getAttenders().clear(); + + lessonRepository.delete(c); + return new ResponseEntity<>(c, HttpStatus.OK); + } + + + + @RequestMapping(value = "/edit/add-attenders/lesson/{lessonId}", method = RequestMethod.PUT) + public ResponseEntity addAttenders( + @RequestBody String[] attenderEmails, + @PathVariable(value="lessonId") String lessonId) + { + + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + long id_lesson = -1; + try{ + id_lesson = Long.parseLong(lessonId); + }catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + Lesson c = lessonRepository.findOne(id_lesson); + + checkAuthorization(c, c.getTeacher()); + + //Strings with a valid email format + Set attenderEmailsValid = new HashSet<>(); + //Strings with an invalid email format + Set attenderEmailsInvalid = new HashSet<>(); + //Strings with a valid email format but no registered in the application + Set attenderEmailsNotRegistered = new HashSet<>(); + + EmailValidator emailValidator = EmailValidator.getInstance(); + + for (int i = 0; i < attenderEmails.length; i++){ + if (emailValidator.isValid(attenderEmails[i])) { + attenderEmailsValid.add(attenderEmails[i]); + } else { + attenderEmailsInvalid.add(attenderEmails[i]); + } + } + + Collection newPossibleAttenders = userRepository.findByNameIn(attenderEmailsValid); + Collection newAddedAttenders = new HashSet<>(); + Collection alreadyAddedAttenders = new HashSet<>(); + + for (String s : attenderEmailsValid){ + if (!this.userListContainsEmail(newPossibleAttenders, s)){ + attenderEmailsNotRegistered.add(s); + } + } + + for (User attender : newPossibleAttenders){ + boolean newAtt = true; + if (!attender.getLessons().contains(c)) attender.getLessons().add(c); else newAtt = false; + if (!c.getAttenders().contains(attender)) c.getAttenders().add(attender); else newAtt = false; + if (newAtt) newAddedAttenders.add(attender); else alreadyAddedAttenders.add(attender); + } + + //Saving the attenders (all of them, just in case a field of the bidirectional relationship is missing in a Lesson or a User) + userRepository.save(newPossibleAttenders); + //Saving the modified lesson + lessonRepository.save(c); + + AddAttendersResponse customResponse = new AddAttendersResponse(); + customResponse.attendersAdded = newAddedAttenders; + customResponse.attendersAlreadyAdded = alreadyAddedAttenders; + customResponse.emailsInvalid = attenderEmailsInvalid; + customResponse.emailsValidNotRegistered = attenderEmailsNotRegistered; + + return new ResponseEntity<>(customResponse, HttpStatus.OK); + } + + + + @RequestMapping(value = "/edit/delete-attenders", method = RequestMethod.PUT) + public ResponseEntity> deleteAttenders(@RequestBody Lesson lesson) { + if (!this.userIsLogged()){ + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + Lesson c = lessonRepository.findOne(lesson.getId()); + + checkAuthorization(c, c.getTeacher()); + + Set setLesson = new HashSet<>(); + setLesson.add(c); + Collection lessonAttenders = userRepository.findByLessons(setLesson); + + for (User attender : lessonAttenders){ + if (!lesson.getAttenders().contains(attender)){ + attender.getLessons().remove(c); + } + } + + userRepository.save(lessonAttenders); + + //Modifying the lesson attenders + c.setAttenders(lesson.getAttenders()); + //Saving the modified lesson + lessonRepository.save(c); + return new ResponseEntity<>(c.getAttenders(), HttpStatus.OK); + } + + + //Login checking method for the backend + private boolean userIsLogged(){ + if (!user.isLoggedUser()) { + System.out.println("Not user logged"); + return false; + } + return true; + } + + //Authorization checking for editing and deleting lessons (the teacher must own the Lesson) + private ResponseEntity checkAuthorization(Object o, User u){ + if(o == null){ + //The object does not exist + return new ResponseEntity<>(HttpStatus.NOT_MODIFIED); + } + if(!this.user.getLoggedUser().equals(u)){ + //The teacher is not authorized to edit it if he is not its owner + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + return null; + } + + //Checks if a User collection contains a user with certain email + private boolean userListContainsEmail(Collection users, String email){ + boolean isContained = false; + for (User u : users){ + if (u.getName().equals(email)) { + isContained = true; + break; + } + } + return isContained; + } + +} + + + diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonRepository.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonRepository.java new file mode 100644 index 00000000..f7ce0582 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/lesson/LessonRepository.java @@ -0,0 +1,13 @@ +package openvidu.openvidu_sample_app.lesson; + +import java.util.Collection; + +import org.springframework.data.jpa.repository.JpaRepository; + +import openvidu.openvidu_sample_app.user.User; + +public interface LessonRepository extends JpaRepository { + + public Collection findByAttenders(Collection users); + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/LoginController.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/LoginController.java new file mode 100644 index 00000000..afe92f91 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/LoginController.java @@ -0,0 +1,66 @@ +package openvidu.openvidu_sample_app.security; + +import javax.servlet.http.HttpSession; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import openvidu.openvidu_sample_app.user.User; +import openvidu.openvidu_sample_app.user.UserComponent; + +/** + * This class is used to provide REST endpoints to logIn and logOut to the + * service. These endpoints are used by Angular 2 SPA client application. + * + * NOTE: This class is not intended to be modified by app developer. + */ +@RestController +public class LoginController { + + private static final Logger log = LoggerFactory.getLogger(LoginController.class); + + @Autowired + private UserComponent userComponent; + + @RequestMapping("/api-logIn") + public ResponseEntity logIn() { + + System.out.println("Logging in..."); + + if (!userComponent.isLoggedUser()) { + + System.out.println("Not user logged"); + + log.info("Not user logged"); + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } else { + User loggedUser = userComponent.getLoggedUser(); + + System.out.println("Logged as " + loggedUser.getName()); + + log.info("Logged as " + loggedUser.getName()); + return new ResponseEntity<>(loggedUser, HttpStatus.OK); + } + } + + @RequestMapping("/api-logOut") + public ResponseEntity logOut(HttpSession session) { + + System.out.println("Logging out..."); + + if (!userComponent.isLoggedUser()) { + log.info("No user logged"); + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } else { + session.invalidate(); + log.info("Logged out"); + return new ResponseEntity<>(true, HttpStatus.OK); + } + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/SecurityConfig.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/SecurityConfig.java new file mode 100644 index 00000000..2c7b1196 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/SecurityConfig.java @@ -0,0 +1,70 @@ +package openvidu.openvidu_sample_app.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * Security configuration. In this class several aspects + * related to security can be configured: + * Security behavior: Login method, session management, CSRF, etc.. + * Authentication provider: Responsible to authenticate users. In this + * example, we use an instance of UserRepositoryAuthProvider, that authenticate + * users stored in a Spring Data database. + * URL Access Authorization: Access to http URLs depending on Authenticated + * vs anonymous users and also based on user role. + * + * + * NOTE: The only part of this class intended for app developer customization is + * the method configureUrlAuthorization. App developer should + * decide what URLs are accessible by what user role. + */ +@Configuration +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Autowired + public UserRepositoryAuthProvider userRepoAuthProvider; + + @Override + protected void configure(HttpSecurity http) throws Exception { + + configureUrlAuthorization(http); + + // Disable CSRF protection (it is difficult to implement with ng2) + http.csrf().disable(); + + // Use Http Basic Authentication + http.httpBasic(); + + // Do not redirect when logout + http.logout().logoutSuccessHandler((rq, rs, a) -> { + }); + } + + private void configureUrlAuthorization(HttpSecurity http) throws Exception { + + // APP: This rules have to be changed by app developer + + // URLs that need authentication to access to it + //Lessons API + http.authorizeRequests().antMatchers(HttpMethod.GET, "/api-lessons/**").hasAnyRole("TEACHER", "STUDENT"); + http.authorizeRequests().antMatchers(HttpMethod.POST, "/api-lessons/**").hasRole("TEACHER"); + http.authorizeRequests().antMatchers(HttpMethod.PUT, "/api-lessons/**").hasRole("TEACHER"); + http.authorizeRequests().antMatchers(HttpMethod.DELETE, "/api-lessons/**").hasRole("TEACHER"); + + http.authorizeRequests().antMatchers(HttpMethod.POST, "/api-sessions/**").authenticated(); + + // Other URLs can be accessed without authentication + http.authorizeRequests().anyRequest().permitAll(); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + + // Database authentication provider + auth.authenticationProvider(userRepoAuthProvider); + } +} \ No newline at end of file diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/UserRepositoryAuthProvider.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/UserRepositoryAuthProvider.java new file mode 100644 index 00000000..58c0ffa0 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/security/UserRepositoryAuthProvider.java @@ -0,0 +1,68 @@ +package openvidu.openvidu_sample_app.security; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Component; + +import openvidu.openvidu_sample_app.user.UserRepository; +import openvidu.openvidu_sample_app.user.UserComponent; +import openvidu.openvidu_sample_app.user.User; +/** + * This class is used to check http credentials against database data. Also it + * is responsible to set database user info into userComponent, a session scoped + * bean that holds session user information. + * + * NOTE: This class is not intended to be modified by app developer. + */ +@Component +public class UserRepositoryAuthProvider implements AuthenticationProvider { + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserComponent userComponent; + + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + + String username = authentication.getName(); + String password = (String) authentication.getCredentials(); + + User user = userRepository.findByName(username); + + if (user == null) { + throw new BadCredentialsException("User not found"); + } + + if (!new BCryptPasswordEncoder().matches(password, user.getPasswordHash())) { + + throw new BadCredentialsException("Wrong password"); + } else { + + userComponent.setLoggedUser(user); + + List roles = new ArrayList<>(); + for (String role : user.getRoles()) { + roles.add(new SimpleGrantedAuthority(role)); + } + + return new UsernamePasswordAuthenticationToken(username, password, roles); + } + } + + @Override + public boolean supports(Class authenticationObject) { + return true; + } +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/session_manager/SessionController.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/session_manager/SessionController.java new file mode 100644 index 00000000..ec93c4ec --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/session_manager/SessionController.java @@ -0,0 +1,204 @@ +package openvidu.openvidu_sample_app.session_manager; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.http.HttpResponse; +import org.apache.http.auth.AuthScope; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.CredentialsProvider; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.conn.ssl.SSLConnectionSocketFactory; +import org.apache.http.conn.ssl.TrustSelfSignedStrategy; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.BasicCredentialsProvider; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.ssl.SSLContextBuilder; +import org.json.simple.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import openvidu.openvidu_sample_app.lesson.Lesson; +import openvidu.openvidu_sample_app.lesson.LessonRepository; +import openvidu.openvidu_sample_app.user.User; +import openvidu.openvidu_sample_app.user.UserComponent; + +@RestController +@RequestMapping("/api-sessions") +public class SessionController { + + @Autowired + private LessonRepository lessonRepository; + + @Autowired + private UserComponent user; + + private Map sessionIdMap = new ConcurrentHashMap<>(); + + private final String OPENVIDU_URL = "https://localhost:8443/"; + private final String SECRET ="MY_SECRET"; + private HttpClient myHttpClient; + + public SessionController() { + try { + CredentialsProvider provider = new BasicCredentialsProvider(); + UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("OPENVIDUAPP", SECRET); + provider.setCredentials(AuthScope.ANY, credentials); + + SSLContextBuilder builder = new SSLContextBuilder(); + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(builder.build(), + SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + /*SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + builder.build());*/ + this.myHttpClient = HttpClients.custom().setSSLSocketFactory( + sslsf).setDefaultCredentialsProvider(provider).build(); + } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) { + e.printStackTrace(); + } + } + + @RequestMapping(value = "/create-session", method = RequestMethod.POST) + public ResponseEntity createSession(@RequestBody String lessonId) throws Exception { + + if (!this.userIsLogged()) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + if(!user.hasRoleTeacher()) { + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + long id_lesson = -1; + try { + id_lesson = Long.parseLong(lessonId); + } catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + Lesson c = lessonRepository.findOne(id_lesson); + + if (!checkAuthorization(c, c.getTeacher())){ + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + if(this.sessionIdMap.get(id_lesson) != null) { + + // If there's already a valid sessionId for this lesson, not necessary to ask for a new one + return new ResponseEntity<>(this.sessionIdMap.get(id_lesson), HttpStatus.OK); + + } else { + + /* + String sessionId = this.openVidu.createSession(); + */ + + HttpResponse response = myHttpClient.execute(new HttpGet(OPENVIDU_URL + "getSessionId")); + + int statusCode = response.getStatusLine().getStatusCode(); + if ((statusCode == org.apache.http.HttpStatus.SC_OK) && (response.getEntity().getContentLength() > 0)){ + System.out.println("Returning a sessionId..."); + BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String sessionId = br.readLine(); + + this.sessionIdMap.put(id_lesson, sessionId); + + return new ResponseEntity<>(sessionId, HttpStatus.OK); + } else { + System.out.println("Problems with openvidu-server"); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + } + + @RequestMapping(value = "/generate-token", method = RequestMethod.POST) + public ResponseEntity generateToken(@RequestBody String lessonId) throws Exception { + + if (!this.userIsLogged()) { + return new ResponseEntity<>(HttpStatus.UNAUTHORIZED); + } + + long id_lesson = -1; + try{ + id_lesson = Long.parseLong(lessonId); + }catch(NumberFormatException e){ + return new ResponseEntity<>(HttpStatus.BAD_REQUEST); + } + + Lesson c = lessonRepository.findOne(id_lesson); + + if (!checkAuthorizationUsers(c, c.getAttenders())){ + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + if (this.sessionIdMap.get(id_lesson) == null){ + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + + String role = user.hasRoleTeacher() ? "PUBLISHER" : "SUBSCRIBER"; + + /* + String token = this.openVidu.generateToken(sessionId, role); + */ + + JSONObject json = new JSONObject(); + json.put(0, this.sessionIdMap.get(id_lesson)); + json.put(1, role); + + HttpPost request = new HttpPost(OPENVIDU_URL + "newToken"); + StringEntity params = new StringEntity(json.toString()); + request.addHeader("content-type", "application/json"); + request.setEntity(params); + + HttpResponse response = myHttpClient.execute(request); + + int statusCode = response.getStatusLine().getStatusCode(); + if ((statusCode == org.apache.http.HttpStatus.SC_OK) && (response.getEntity().getContentLength() > 0)){ + System.out.println("Returning a sessionId and a token..."); + BufferedReader br = new BufferedReader(new InputStreamReader(response.getEntity().getContent())); + String token = br.readLine(); + + JSONObject responseJson = new JSONObject(); + responseJson.put(0, this.sessionIdMap.get(id_lesson)); + responseJson.put(1, token); + + return new ResponseEntity<>(responseJson, HttpStatus.OK); + } else { + System.out.println("Problems with openvidu-server"); + return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + + private boolean userIsLogged(){ + if (!user.isLoggedUser()) { + System.out.println("Not user logged"); + return false; + } + return true; + } + + // Authorization checking for creating or joining a certain lesson + private boolean checkAuthorization(Object o, User u){ + return !(o == null || !this.user.getLoggedUser().equals(u)); + } + + // Authorization checking for joining a session (the user must be an attender) + private boolean checkAuthorizationUsers(Object o, Collection users){ + return !(o == null || !users.contains(this.user.getLoggedUser())); + } + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/User.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/User.java new file mode 100644 index 00000000..6174df93 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/User.java @@ -0,0 +1,118 @@ +package openvidu.openvidu_sample_app.user; + +import java.util.List; +import java.util.Set; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.GenerationType; +import javax.persistence.ElementCollection; +import javax.persistence.Id; +import javax.persistence.ManyToMany; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import openvidu.openvidu_sample_app.lesson.Lesson; + +@Entity +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + private long id; + + private String name; + + private String nickName; + + @JsonIgnore + private String passwordHash; + + @ElementCollection(fetch = FetchType.EAGER) + private List roles; + + //It is ignored in order to avoid infinite recursiveness + //This makes necessary another interaction with the database (after login to retrieve the lessons of the user) + @JsonIgnore + @ManyToMany(mappedBy="attenders") + private Set lessons; + + public User() {} + + public User(String name, String password, String nickName, String... roles){ + this.name = name; + this.passwordHash = new BCryptPasswordEncoder().encode(password); + this.roles = new ArrayList<>(Arrays.asList(roles)); + this.nickName = nickName; + this.lessons = new HashSet<>(); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public String getNickName() { + return nickName; + } + + public void setNickName(String nickName) { + this.nickName = nickName; + } + + public Set getLessons() { + return lessons; + } + + public void setLessons(Set lessons) { + this.lessons = lessons; + } + + @Override + public boolean equals(Object other){ + if (other == null) return false; + if (other == this) return true; + if (!(other instanceof User))return false; + User otherUser = (User)other; + return ((otherUser.id == this.id) && (otherUser.name.equals(this.name))); + } + + @Override + public int hashCode() { + return name.hashCode(); + } + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserComponent.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserComponent.java new file mode 100644 index 00000000..893ea3a9 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserComponent.java @@ -0,0 +1,43 @@ +package openvidu.openvidu_sample_app.user; + +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; +import org.springframework.stereotype.Component; +import org.springframework.web.context.WebApplicationContext; + +/** + * This class is designed to manage the information for the user while he is + * logged in the service. This object can be used in any other @Component + * auto-wiring it as usual. + * + * Instances of this class are never sent to the user in any REST endpoint. It + * can hold sensible information that can not be known in the client. + * + * NOTE: This class is intended to be extended by developer adding new + * attributes. Current attributes can not be removed because they are used in + * authentication procedures. + */ + +@Component +@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS) +public class UserComponent { + + private User user; + + public User getLoggedUser() { + return user; + } + + public void setLoggedUser(User user) { + this.user = user; + } + + public boolean isLoggedUser() { + return this.user != null; + } + + public boolean hasRoleTeacher() { + return this.user.getRoles().contains("ROLE_TEACHER"); + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserController.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserController.java new file mode 100644 index 00000000..d6c30e3d --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserController.java @@ -0,0 +1,56 @@ +package openvidu.openvidu_sample_app.user; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import org.apache.commons.validator.routines.EmailValidator; + + +@RestController +@RequestMapping("/api-users") +public class UserController { + + @Autowired + private UserRepository userRepository; + + //userData: [name, pass, nickName, role] + @RequestMapping(value = "/new", method = RequestMethod.POST) + public ResponseEntity newUser(@RequestBody String[] userData) throws Exception { + + System.out.println("Signing up a user..."); + + //If the email is not already in use + if(userRepository.findByName(userData[0]) == null) { + + //If the email has a valid format + if (EmailValidator.getInstance().isValid(userData[0])){ + String role = (String) userData[3]; + System.out.println("Email and password are valid. Role of the new user: " + role); + if(role == null){ + userData[3] = "ROLE_STUDENT"; + } else if (role.equals("teacher")) { + userData[3] = "ROLE_TEACHER"; + } else { + userData[3] = "ROLE_STUDENT"; + } + User newUser = new User(userData[0], userData[1], userData[2], userData[3]); + userRepository.save(newUser); + return new ResponseEntity<>(newUser, HttpStatus.CREATED); + } + else { + System.out.println("Email NOT valid"); + return new ResponseEntity<>(HttpStatus.FORBIDDEN); + } + + } else { + System.out.println("Email already in use"); + return new ResponseEntity<>(HttpStatus.CONFLICT); + } + } + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserRepository.java b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserRepository.java new file mode 100644 index 00000000..362815a1 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/java/openvidu/openvidu_sample_app/user/UserRepository.java @@ -0,0 +1,17 @@ +package openvidu.openvidu_sample_app.user; + +import java.util.Collection; + +import org.springframework.data.jpa.repository.JpaRepository; + +import openvidu.openvidu_sample_app.lesson.Lesson; + +public interface UserRepository extends JpaRepository{ + + public User findByName(String name); + + public Collection findByNameIn(Collection names); + + public Collection findByLessons(Collection lessons); + +} diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/main/resources/application.properties b/openvidu-sample-app/backend/openvidu-sample-app/src/main/resources/application.properties new file mode 100644 index 00000000..cb248ed6 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/main/resources/application.properties @@ -0,0 +1,7 @@ +spring.datasource.url=jdbc:mysql://localhost/full_teaching +spring.datasource.username=ft-root +spring.datasource.password=pass +spring.datasource.driverClassName=com.mysql.jdbc.Driver +spring.jpa.hibernate.ddl-auto: create-drop + +server.port=5000 diff --git a/openvidu-sample-app/backend/openvidu-sample-app/src/test/java/openvidu/openvidu_sample_app/AppTest.java b/openvidu-sample-app/backend/openvidu-sample-app/src/test/java/openvidu/openvidu_sample_app/AppTest.java new file mode 100644 index 00000000..e1ceab17 --- /dev/null +++ b/openvidu-sample-app/backend/openvidu-sample-app/src/test/java/openvidu/openvidu_sample_app/AppTest.java @@ -0,0 +1,38 @@ +package openvidu.openvidu_sample_app; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +} diff --git a/openvidu-sample-app/frontend/.angular-cli.json b/openvidu-sample-app/frontend/.angular-cli.json new file mode 100644 index 00000000..ac198067 --- /dev/null +++ b/openvidu-sample-app/frontend/.angular-cli.json @@ -0,0 +1,57 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "project": { + "name": "openvidu-sample-app" + }, + "apps": [ + { + "root": "src", + "outDir": "dist", + "assets": [ + "assets", + "favicon.ico" + ], + "index": "index.html", + "main": "main.ts", + "polyfills": "polyfills.ts", + "test": "test.ts", + "tsconfig": "tsconfig.app.json", + "testTsconfig": "tsconfig.spec.json", + "prefix": "app", + "styles": [ + "styles.css" + ], + "scripts": [], + "environmentSource": "environments/environment.ts", + "environments": { + "dev": "environments/environment.ts", + "prod": "environments/environment.prod.ts" + } + } + ], + "e2e": { + "protractor": { + "config": "./protractor.conf.js" + } + }, + "lint": [ + { + "project": "src/tsconfig.app.json" + }, + { + "project": "src/tsconfig.spec.json" + }, + { + "project": "e2e/tsconfig.e2e.json" + } + ], + "test": { + "karma": { + "config": "./karma.conf.js" + } + }, + "defaults": { + "styleExt": "css", + "component": {} + } +} diff --git a/openvidu-sample-app/frontend/.editorconfig b/openvidu-sample-app/frontend/.editorconfig new file mode 100644 index 00000000..6e87a003 --- /dev/null +++ b/openvidu-sample-app/frontend/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/openvidu-sample-app/frontend/.gitignore b/openvidu-sample-app/frontend/.gitignore new file mode 100644 index 00000000..1b0e36c4 --- /dev/null +++ b/openvidu-sample-app/frontend/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# compiled output +/dist +/tmp + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/ +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage/* +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +# e2e +/e2e/*.js +/e2e/*.map + +#System Files +.DS_Store +Thumbs.db diff --git a/openvidu-sample-app/frontend/README.md b/openvidu-sample-app/frontend/README.md new file mode 100644 index 00000000..157baac5 --- /dev/null +++ b/openvidu-sample-app/frontend/README.md @@ -0,0 +1,27 @@ +# OpenviduSampleApp + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.0.0-rc.1. + +## Development server +Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. + +## Code scaffolding + +Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). +Before running the tests make sure you are serving the app via `ng serve`. + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). diff --git a/openvidu-sample-app/frontend/e2e/app.e2e-spec.ts b/openvidu-sample-app/frontend/e2e/app.e2e-spec.ts new file mode 100644 index 00000000..bf90d623 --- /dev/null +++ b/openvidu-sample-app/frontend/e2e/app.e2e-spec.ts @@ -0,0 +1,14 @@ +import { OpenviduSampleAppPage } from './app.po'; + +describe('openvidu-sample-app App', () => { + let page: OpenviduSampleAppPage; + + beforeEach(() => { + page = new OpenviduSampleAppPage(); + }); + + it('should display message saying app works', () => { + page.navigateTo(); + expect(page.getParagraphText()).toEqual('app works!'); + }); +}); diff --git a/openvidu-sample-app/frontend/e2e/app.po.ts b/openvidu-sample-app/frontend/e2e/app.po.ts new file mode 100644 index 00000000..b0c448b2 --- /dev/null +++ b/openvidu-sample-app/frontend/e2e/app.po.ts @@ -0,0 +1,11 @@ +import { browser, element, by } from 'protractor'; + +export class OpenviduSampleAppPage { + navigateTo() { + return browser.get('/'); + } + + getParagraphText() { + return element(by.css('app-root h1')).getText(); + } +} diff --git a/openvidu-sample-app/frontend/e2e/tsconfig.e2e.json b/openvidu-sample-app/frontend/e2e/tsconfig.e2e.json new file mode 100644 index 00000000..74c2bca1 --- /dev/null +++ b/openvidu-sample-app/frontend/e2e/tsconfig.e2e.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2016" + ], + "outDir": "../dist/out-tsc-e2e", + "module": "commonjs", + "target": "es6", + "types":[ + "jasmine", + "node" + ] + } +} diff --git a/openvidu-sample-app/frontend/karma.conf.js b/openvidu-sample-app/frontend/karma.conf.js new file mode 100644 index 00000000..84b4cd5a --- /dev/null +++ b/openvidu-sample-app/frontend/karma.conf.js @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/0.13/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular/cli'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage-istanbul-reporter'), + require('@angular/cli/plugins/karma') + ], + client:{ + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + files: [ + { pattern: './src/test.ts', watched: false } + ], + preprocessors: { + './src/test.ts': ['@angular/cli'] + }, + mime: { + 'text/x-typescript': ['ts','tsx'] + }, + coverageIstanbulReporter: { + reports: [ 'html', 'lcovonly' ], + fixWebpackSourcePaths: true + }, + angularCli: { + environment: 'dev' + }, + reporters: config.angularCli && config.angularCli.codeCoverage + ? ['progress', 'coverage-istanbul'] + : ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false + }); +}; diff --git a/openvidu-sample-app/frontend/package.json b/openvidu-sample-app/frontend/package.json new file mode 100644 index 00000000..064618fd --- /dev/null +++ b/openvidu-sample-app/frontend/package.json @@ -0,0 +1,52 @@ +{ + "name": "openvidu-sample-app", + "version": "0.0.0", + "license": "MIT", + "author": "Pablo Fuente", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "test": "ng test", + "lint": "ng lint", + "e2e": "ng e2e" + }, + "private": true, + "dependencies": { + "@angular/common": "^2.4.0", + "@angular/compiler": "^2.4.0", + "@angular/core": "^2.4.0", + "@angular/flex-layout": "^2.0.0-rc.1", + "@angular/forms": "^2.4.0", + "@angular/http": "^2.4.0", + "@angular/material": "2.0.0-beta.2", + "@angular/platform-browser": "^2.4.0", + "@angular/platform-browser-dynamic": "^2.4.0", + "@angular/router": "^3.4.0", + "core-js": "^2.4.1", + "hammerjs": "^2.0.8", + "jquery": "^2.2.4", + "openvidu-browser": "0.1.1", + "rxjs": "^5.1.0", + "zone.js": "^0.7.6" + }, + "devDependencies": { + "@angular/cli": "1.0.0-rc.1", + "@angular/compiler-cli": "^2.4.0", + "@types/jasmine": "2.5.38", + "@types/node": "~6.0.60", + "codelyzer": "~2.0.0", + "jasmine-core": "~2.5.2", + "jasmine-spec-reporter": "~3.2.0", + "karma": "~1.4.1", + "karma-chrome-launcher": "~2.0.0", + "karma-cli": "~1.0.1", + "karma-jasmine": "~1.1.0", + "karma-jasmine-html-reporter": "^0.2.2", + "karma-coverage-istanbul-reporter": "^0.2.0", + "protractor": "~5.1.0", + "ts-node": "~2.0.0", + "tslint": "~4.4.2", + "typescript": "~2.0.0" + } +} diff --git a/openvidu-sample-app/frontend/protractor.conf.js b/openvidu-sample-app/frontend/protractor.conf.js new file mode 100644 index 00000000..1c5e1e5a --- /dev/null +++ b/openvidu-sample-app/frontend/protractor.conf.js @@ -0,0 +1,30 @@ +// Protractor configuration file, see link for more information +// https://github.com/angular/protractor/blob/master/lib/config.ts + +const { SpecReporter } = require('jasmine-spec-reporter'); + +exports.config = { + allScriptsTimeout: 11000, + specs: [ + './e2e/**/*.e2e-spec.ts' + ], + capabilities: { + 'browserName': 'chrome' + }, + directConnect: true, + baseUrl: 'http://localhost:4200/', + framework: 'jasmine', + jasmineNodeOpts: { + showColors: true, + defaultTimeoutInterval: 30000, + print: function() {} + }, + beforeLaunch: function() { + require('ts-node').register({ + project: 'e2e/tsconfig.e2e.json' + }); + }, + onPrepare() { + jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); + } +}; diff --git a/openvidu-sample-app/frontend/src/app/app.component.css b/openvidu-sample-app/frontend/src/app/app.component.css new file mode 100644 index 00000000..1105196c --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.component.css @@ -0,0 +1,31 @@ +md-sidenav { + width: 250px; +} + +md-sidenav-container { + height: 100%; +} + +footer.page-footer { + margin: 0; +} + +footer h2 { + margin-top: 10px; +} + +.sidenav-button { + width: 100%; +} + +header .fill-remaining-space { + flex: 1 1 auto; +} + +header #navbar-logo { + font-weight: bold; +} + +footer ul { + padding-left: 0; +} diff --git a/openvidu-sample-app/frontend/src/app/app.component.html b/openvidu-sample-app/frontend/src/app/app.component.html new file mode 100644 index 00000000..29057c04 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.component.html @@ -0,0 +1,60 @@ + + + + + + + + +
+ + + +
+ + + +
+ +
+
+ +
+ +
+ + + +
diff --git a/openvidu-sample-app/frontend/src/app/app.component.spec.ts b/openvidu-sample-app/frontend/src/app/app.component.spec.ts new file mode 100644 index 00000000..c740bcd7 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.component.spec.ts @@ -0,0 +1,32 @@ +import { TestBed, async } from '@angular/core/testing'; + +import { AppComponent } from './app.component'; + +describe('AppComponent', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + AppComponent + ], + }).compileComponents(); + })); + + it('should create the app', async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app).toBeTruthy(); + })); + + it(`should have as title 'app works!'`, async(() => { + const fixture = TestBed.createComponent(AppComponent); + const app = fixture.debugElement.componentInstance; + expect(app.title).toEqual('app works!'); + })); + + it('should render title in a h1 tag', async(() => { + const fixture = TestBed.createComponent(AppComponent); + fixture.detectChanges(); + const compiled = fixture.debugElement.nativeElement; + expect(compiled.querySelector('h1').textContent).toContain('app works!'); + })); +}); diff --git a/openvidu-sample-app/frontend/src/app/app.component.ts b/openvidu-sample-app/frontend/src/app/app.component.ts new file mode 100644 index 00000000..f7748e4c --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.component.ts @@ -0,0 +1,18 @@ +import { Component } from '@angular/core'; +import { Router } from '@angular/router'; + +import { AuthenticationService } from './services/authentication.service'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + + constructor(private router: Router, private authenticationService: AuthenticationService) { } + + isVideoSessionUrl() { + return (this.router.url.substring(0, '/lesson/'.length) === '/lesson/'); + } +} diff --git a/openvidu-sample-app/frontend/src/app/app.module.ts b/openvidu-sample-app/frontend/src/app/app.module.ts new file mode 100644 index 00000000..2d2a9630 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.module.ts @@ -0,0 +1,59 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { HttpModule } from '@angular/http'; + +import { routing } from './app.routing'; + +import { MaterialModule } from '@angular/material'; +import { FlexLayoutModule } from '@angular/flex-layout'; +import 'hammerjs'; + +import { AppComponent } from './app.component'; +import { PresentationComponent } from './components/presentation/presentation.component'; +import { DashboardComponent } from './components/dashboard/dahsboard.component'; +import { LessonDetailsComponent } from './components/lesson-details/lesson-details.component'; +import { ProfileComponent } from './components/profile/profile.component'; +import { VideoSessionComponent } from './components/video-session/video-session.component'; +import { ErrorMessageComponent } from './components/error-message/error-message.component'; +import { JoinSessionDialogComponent } from './components/dashboard/join-session-dialog.component'; + +import { AuthenticationService } from './services/authentication.service'; +import { UserService } from './services/user.service'; +import { LessonService } from './services/lesson.service'; +import { VideoSessionService } from './services/video-session.service'; +import { AuthGuard } from './auth.guard'; + + +@NgModule({ + declarations: [ + AppComponent, + PresentationComponent, + DashboardComponent, + LessonDetailsComponent, + ProfileComponent, + VideoSessionComponent, + ErrorMessageComponent, + JoinSessionDialogComponent, + ], + imports: [ + BrowserModule, + FormsModule, + HttpModule, + MaterialModule, + FlexLayoutModule.forRoot(), + routing, + ], + providers: [ + AuthenticationService, + UserService, + LessonService, + VideoSessionService, + AuthGuard, + ], + entryComponents: [ + JoinSessionDialogComponent, + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/openvidu-sample-app/frontend/src/app/app.routing.ts b/openvidu-sample-app/frontend/src/app/app.routing.ts new file mode 100644 index 00000000..78f1249f --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/app.routing.ts @@ -0,0 +1,40 @@ +import { ModuleWithProviders } from '@angular/core'; +import { Routes, RouterModule } from '@angular/router'; + +import { PresentationComponent } from './components/presentation/presentation.component'; +import { DashboardComponent } from './components/dashboard/dahsboard.component'; +import { LessonDetailsComponent } from './components/lesson-details/lesson-details.component'; +import { ProfileComponent } from './components/profile/profile.component'; +import { VideoSessionComponent } from './components/video-session/video-session.component'; + +import { AuthGuard } from './auth.guard'; + +const appRoutes: Routes = [ + { + path: '', + component: PresentationComponent, + pathMatch: 'full', + }, + { + path: 'lessons', + component: DashboardComponent, + canActivate: [AuthGuard] + }, + { + path: 'lesson-details/:id', + component: LessonDetailsComponent, + canActivate: [AuthGuard] + }, + { + path: 'profile', + component: ProfileComponent, + canActivate: [AuthGuard] + }, + { + path: 'lesson/:id', + component: VideoSessionComponent, + canActivate: [AuthGuard] + }, +]; + +export const routing: ModuleWithProviders = RouterModule.forRoot(appRoutes); diff --git a/openvidu-sample-app/frontend/src/app/auth.guard.ts b/openvidu-sample-app/frontend/src/app/auth.guard.ts new file mode 100644 index 00000000..b8159652 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/auth.guard.ts @@ -0,0 +1,21 @@ +import { Injectable } from '@angular/core'; +import { Router, CanActivate } from '@angular/router'; + +import { AuthenticationService } from './services/authentication.service'; + +@Injectable() +export class AuthGuard implements CanActivate { + + constructor(private router: Router, private authenticationService: AuthenticationService) { } + + canActivate() { + if (localStorage.getItem('login') && localStorage.getItem('rol') && this.authenticationService.isLoggedIn()) { + // logged in so return true + return true; + } + + // not logged in so redirect to login page + this.router.navigate(['']); + return false; + } +} diff --git a/openvidu-sample-app/frontend/src/app/components/dashboard/dahsboard.component.ts b/openvidu-sample-app/frontend/src/app/components/dashboard/dahsboard.component.ts new file mode 100644 index 00000000..af534f9b --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/dashboard/dahsboard.component.ts @@ -0,0 +1,117 @@ +import { Component, OnInit, EventEmitter } from '@angular/core'; +import { Router } from '@angular/router'; +import { MdDialog, MdDialogRef, MdSnackBar } from '@angular/material'; + +import { Lesson } from '../../models/lesson'; + +import { LessonService } from '../../services/lesson.service'; +import { VideoSessionService } from '../../services/video-session.service'; +import { AuthenticationService } from '../../services/authentication.service'; + +import { JoinSessionDialogComponent } from './join-session-dialog.component'; + +@Component({ + selector: 'app-dashboard', + templateUrl: './dashboard.component.html', + styleUrls: ['./dashboard.component.css'], +}) +export class DashboardComponent implements OnInit { + + lessons: Lesson[]; + + addingLesson: false; + lessonTitle: string; + sumbitNewLesson: boolean; + + constructor( + private lessonService: LessonService, + private videoSessionService: VideoSessionService, + private authenticationService: AuthenticationService, + private router: Router, + public snackBar: MdSnackBar, + public dialog: MdDialog + ) { } + + ngOnInit(): void { + this.authenticationService.checkCredentials(); + this.getLessons(); + } + + logout() { + this.authenticationService.logOut(); + } + + getLessons(): void { + this.lessonService.getLessons(this.authenticationService.getCurrentUser()).subscribe( + lessons => { + console.log('User\'s lessons: '); + console.log(lessons); + this.lessons = lessons; + this.authenticationService.updateUserLessons(this.lessons); + }, + error => console.log(error)); + } + + goToLesson(lesson: Lesson) { + let dialogRef: MdDialogRef; + dialogRef = this.dialog.open(JoinSessionDialogComponent); + dialogRef.componentInstance.myReference = dialogRef; + + dialogRef.afterClosed().subscribe(cameraOptions => { + if (cameraOptions != null) { + console.log('Joining session with options:'); + console.log(cameraOptions); + this.videoSessionService.lesson = lesson; + this.videoSessionService.cameraOptions = cameraOptions; + this.router.navigate(['/lesson/' + lesson.id]); + } + }); + } + + goToLessonDetails(lesson: Lesson) { + this.router.navigate(['/lesson-details/' + lesson.id]); + } + + newLesson() { + this.sumbitNewLesson = true; + this.lessonService.newLesson(new Lesson(this.lessonTitle)).subscribe( + lesson => { + console.log('New lesson added: '); + console.log(lesson); + this.lessons.push(lesson); + this.authenticationService.updateUserLessons(this.lessons); + this.sumbitNewLesson = false; + this.snackBar.open('Lesson added!', undefined, { duration: 3000 }); + this.addingLesson = false; + }, + error => { + console.log(error); + this.sumbitNewLesson = false; + this.snackBar.open('There has been a problem...', undefined, { duration: 3000 }); + } + ); + } + + createSession(lessonId: number){ + this.videoSessionService.createSession(lessonId).subscribe( + response => { + console.log(response.text()); + }, + error => { + console.log(error); + } + ) + } + + generateToken(lessonId: number) { + this.videoSessionService.generateToken(lessonId).subscribe( + response => { + console.log(response.text()); + }, + error => { + console.log(error); + } + ) + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.css b/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.css new file mode 100644 index 00000000..951f93b7 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.css @@ -0,0 +1,11 @@ +md-card { + margin-top: 20px; +} + +md-card md-icon { + text-align: center; +} + +span.teacher { + font-size: 12px; +} diff --git a/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.html b/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.html new file mode 100644 index 00000000..794cfcd1 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/dashboard/dashboard.component.html @@ -0,0 +1,48 @@ +
+
+
+ +
+
+ +
+
MY LESSONS
+ add_circle_outline +
+ +
+
NEW LESSON
+
+ + + +
+ + +
+
+
+ + +
+ {{lesson.title}} + {{lesson.teacher.nickName}} + mode_edit + play_circle_filled +
+
+ +
+ +
+ +
+ +
+ +
+
diff --git a/openvidu-sample-app/frontend/src/app/components/dashboard/join-session-dialog.component.ts b/openvidu-sample-app/frontend/src/app/components/dashboard/join-session-dialog.component.ts new file mode 100644 index 00000000..76e33b4d --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/dashboard/join-session-dialog.component.ts @@ -0,0 +1,111 @@ +import { Component } from '@angular/core'; +import { MdDialogRef } from '@angular/material'; + +@Component({ + selector: 'app-join-session-dialog', + template: ` +
+

+ Video options +

+
+ +
+
Quality
+ + Low + Medium + High + Very high + +
+
+
Enter with active...
+ Video + Audio +
+
+ + + + +
+
+ `, + styles: [` + #quality-div { + margin-top: 20px; + } + #join-div { + margin-top: 25px; + margin-bottom: 20px; + } + #quality-tag { + display: block; + } + h5 { + margin-bottom: 10px; + text-align: left; + } + #joinWithVideo { + margin-right: 50px; + } + md-dialog-actions { + display: block; + } + #join-btn { + float: right; + } + `], +}) +export class JoinSessionDialogComponent { + + public myReference: MdDialogRef; + private quality = 'medium'; + private joinWithVideo = true; + private joinWithAudio = true; + + constructor() { } + + joinSession() { + let cameraOptions = { + audio: this.joinWithAudio, + video: this.joinWithVideo, + data: true, + mediaConstraints: this.generateMediaConstraints() + }; + this.myReference.close(cameraOptions); + } + + generateMediaConstraints() { + let mediaConstraints = { + audio: true, + video: {} + } + let w = 640; + let h = 480; + switch (this.quality) { + case 'low': + w = 320; + h = 240; + break; + case 'medium': + w = 640; + h = 480; + break; + case 'high': + w = 1280; + h = 720; + break; + case 'veryhigh': + w = 1920; + h = 1080; + break; + } + mediaConstraints.video['width'] = { exact: w }; + mediaConstraints.video['height'] = { exact: h }; + //mediaConstraints.video['frameRate'] = { ideal: Number((document.getElementById('frameRate')).value) }; + + return mediaConstraints; + } +} \ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.css b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.css new file mode 100644 index 00000000..bf2771f0 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.css @@ -0,0 +1,26 @@ +.fail { + background-color: rgba(167, 56, 65, 0.2); + color: #a73841; +} + +.warning { + background-color: rgba(175, 110, 0, 0.2); + color: #af6e00; +} + +.correct { + background-color: rgba(55, 86, 70, 0.25); + color: #375546; +} + +md-icon { + cursor: pointer; + float: right; +} + +md-card { + max-width: 400px; + margin-top: 20px; + margin-bottom: 20px; + box-shadow: none; +} diff --git a/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.html b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.html new file mode 100644 index 00000000..05960dcc --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.html @@ -0,0 +1,5 @@ + + clear + {{this.errorTitle}} + + diff --git a/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.ts b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.ts new file mode 100644 index 00000000..cc4384d5 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/error-message/error-message.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, Output, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-error-message', + templateUrl: './error-message.component.html', + styleUrls: ['./error-message.component.css'] +}) +export class ErrorMessageComponent { + + @Input() + errorTitle: string; + @Input() + errorContent: string; + @Input() + customClass: string; + @Input() + closable: boolean; + @Input() + timeable: number; + + @Output() + eventShowable = new EventEmitter(); + + constructor() { } + + public closeAlert() { + this.eventShowable.emit(false); + } + +} diff --git a/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.css b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.css new file mode 100644 index 00000000..db8e833e --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.css @@ -0,0 +1,73 @@ +.attender-email { + font-size: 11px; +} + +.no-margin-bottom { + margin-bottom: 0 !important; +} + +.attender-row { + width: 100%; + margin-top: 20px; + min-height: 27px; +} + +#new-attender-title { + margin-bottom: 5px; +} + + +/*Rotating animation*/ + +@-webkit-keyframes rotating +/* Safari and Chrome */ + +{ + from { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + to { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +@keyframes rotating { + from { + -ms-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -webkit-transform: rotate(360deg); + -o-transform: rotate(360deg); + transform: rotate(360deg); + } + to { + -ms-transform: rotate(0deg); + -moz-transform: rotate(0deg); + -webkit-transform: rotate(0deg); + -o-transform: rotate(0deg); + transform: rotate(0deg); + } +} + +.rotating { + -webkit-animation: rotating 1s linear infinite; + -moz-animation: rotating 1s linear infinite; + -ms-animation: rotating 1s linear infinite; + -o-animation: rotating 1s linear infinite; + animation: rotating 1s linear infinite; + cursor: default !important; +} + +.rotating:hover { + color: inherit !important; +} + + +/*End rotating animation*/ diff --git a/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.html b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.html new file mode 100644 index 00000000..c2b4cc65 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.html @@ -0,0 +1,66 @@ +
+
+ +
+ keyboard_arrow_left +

{{lesson.title}}

+ mode_edit +
+ +
+
+ + + +
+ + Cancel + Delete lesson +
+
+
+ +
+

New attender

+ + + +
+ + Cancel +
+
+ + + + +
+
+
+
{{authenticationService.getCurrentUser().nickName}}
+
{{authenticationService.getCurrentUser().name}}
+
+
+
+
+
+
+
+
+
{{attender.nickName}}
+
{{attender.name}}
+
+
+
+ clear + cached +
+
+
+ +
+
diff --git a/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.ts b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.ts new file mode 100644 index 00000000..02453615 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/lesson-details/lesson-details.component.ts @@ -0,0 +1,186 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { ActivatedRoute, Params, Router } from '@angular/router'; +import { Location } from '@angular/common'; +import { MdSnackBar } from '@angular/material'; + +import { Lesson } from '../../models/lesson'; +import { User } from '../../models/user'; + +import { LessonService } from '../../services/lesson.service'; +import { AuthenticationService } from '../../services/authentication.service'; + +@Component({ + selector: 'app-lesson-details', + templateUrl: './lesson-details.component.html', + styleUrls: ['./lesson-details.component.css'], +}) +export class LessonDetailsComponent implements OnInit { + + lesson: Lesson; + + editingTitle = false; + titleEdited: string; + sumbitEditLesson = false; + emailAttender: string; + sumbitAddAttenders = false; + arrayOfAttDels = []; + + // Feedback message parameters + addAttendersCorrect = false; + addAttendersError = false; + attErrorTitle: string; + attErrorContent: string; + attCorrectTitle: string; + attCorrectContent: string; + + constructor( + private lessonService: LessonService, + private authenticationService: AuthenticationService, + private router: Router, + private route: ActivatedRoute, + private location: Location, + public snackBar: MdSnackBar + ) { } + + ngOnInit(): void { + this.route.params + .map((params: Params) => this.lessonService.obtainLocalLesson(+params['id'])) + .subscribe(lesson => this.lesson = lesson); + } + + editLesson() { + if (this.titleEdited !== this.lesson.title) { + this.sumbitEditLesson = true; + let l = new Lesson(this.titleEdited); + l.id = this.lesson.id; + this.lessonService.editLesson(l).subscribe( + lesson => { + // Lesson has been updated + console.log('Lesson edited: '); + console.log(lesson); + this.lesson = lesson; + this.sumbitEditLesson = false; + this.editingTitle = false; + this.snackBar.open('Lesson edited!', undefined, { duration: 3000 }); + }, + error => { + console.log(error); + this.sumbitEditLesson = false; + this.snackBar.open('There has been a problem...', undefined, { duration: 3000 }); + }); + } else { + this.editingTitle = false; // The user has not modified the title + } + } + + deleteLesson() { + this.sumbitEditLesson = true; + this.lessonService.deleteLesson(this.lesson.id).subscribe( + lesson => { + // Lesson has been deleted + console.log('Lesson deleted'); + console.log(lesson); + this.sumbitEditLesson = false; + this.router.navigate(['/lessons']); + this.snackBar.open('Lesson deleted!', undefined, { duration: 3000 }); + }, + error => { + console.log(error); + this.sumbitEditLesson = false; + this.snackBar.open('There has been a problem...', undefined, { duration: 3000 }); + }); + } + + addLessonAttenders() { + this.sumbitAddAttenders = true; + this.lessonService.addLessonAttenders(this.lesson.id, [this.emailAttender]).subscribe( + response => { // response: attendersAdded, attendersAlreadyAdded, emailsInvalid, emailsValidNotRegistered + console.log('Attender added'); + console.log(response); + this.sumbitAddAttenders = false; + let newAttenders = response.attendersAdded as User[]; + this.lesson.attenders = this.lesson.attenders.concat(newAttenders); + this.handleAttendersMessage(response); + }, + error => { + console.log(error); + this.sumbitAddAttenders = false; + this.snackBar.open('There has been a problem...', undefined, { duration: 3000 }); + }); + } + + deleteLessonAttender(i: number, attender: User) { + this.arrayOfAttDels[i] = true; + let l = new Lesson(this.lesson.title); + l.id = this.lesson.id; + for (let i = 0; i < this.lesson.attenders.length; i++) { + if (this.lesson.attenders[i].id !== attender.id) { + l.attenders.push(new User(this.lesson.attenders[i])); //Inserting a new User object equal to the attender but "lessons" array empty + } + } + this.lessonService.deleteLessonAttenders(l).subscribe( + attenders => { + console.log('Attender removed'); + console.log(attenders); + this.arrayOfAttDels[i] = false; + this.lesson.attenders = attenders; + this.snackBar.open('Attender removed!', undefined, { duration: 3000 }); + }, + error => { + console.log(error); + this.arrayOfAttDels[i] = false; + this.snackBar.open('There has been a problem...', undefined, { duration: 3000 }); + }); + } + + // Creates an error message when there is any problem during the process of adding Users to a Lesson + // It also generates a correct feedback message when any student has been correctly added to the Lesson + handleAttendersMessage(response) { + let isError: boolean = false; + let isCorrect: boolean = false; + this.attErrorContent = ""; + this.attCorrectContent = ""; + + if (response.attendersAdded.length > 0) { + for (let user of response.attendersAdded) { + this.attCorrectContent += ""; + } + isCorrect = true; + } + if (response.attendersAlreadyAdded.length > 0) { + this.attErrorContent += "The following users were already added to the lesson"; + for (let user of response.attendersAlreadyAdded) { + this.attErrorContent += ""; + } + isError = true; + } + if (response.emailsValidNotRegistered.length > 0) { + this.attErrorContent += "The following users are not registered"; + for (let email of response.emailsValidNotRegistered) { + this.attErrorContent += ""; + } + isError = true; + } + if (response.emailsInvalid) { + if (response.emailsInvalid.length > 0) { + this.attErrorContent += "These are not valid emails"; + for (let email of response.emailsInvalid) { + this.attErrorContent += ""; + } + isError = true; + } + } + if (isError) { + this.attErrorTitle = "There have been some problems"; + this.addAttendersError = true; + } else if (response.attendersAdded.length == 0) { + this.attErrorTitle = "No emails there!"; + this.addAttendersError = true; + } + if (isCorrect) { + this.attCorrectTitle = "The following users where properly added"; + this.addAttendersCorrect = true; + } + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.css b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.css new file mode 100644 index 00000000..d0b4b5d0 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.css @@ -0,0 +1,35 @@ +h1 { + text-align: center; + display: block; +} + +md-input-container { + width: 100%; +} + +md-card-actions { + padding-left: 10px; + padding-right: 10px; + color: #9e9e9e; +} + +.btn-container { + text-align: center; + padding-top: 20px; +} + +.card-button { + margin-left: 10px !important; +} + +.radio-button-div { + text-align: center; + margin-bottom: 10px; +} + +#sign-up-as { + color: #9e9e9e; + display: block; + margin-top: 15px; + margin-bottom: 10px; +} diff --git a/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.html b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.html new file mode 100644 index 00000000..9097c78b --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.html @@ -0,0 +1,71 @@ +
+
+ +

OpenVidu Demo

+ +
+ + + + +
+
+
+ +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ + + +
+ +
+ Sign up as... + + Student + Teacher + +
+ + + +
+ + +
+ +
+
+ + +
Not registered yet?
+
Already registered?
+
+ +
+ +
+ +
+
diff --git a/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.ts b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.ts new file mode 100644 index 00000000..fc4c4afa --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/presentation/presentation.component.ts @@ -0,0 +1,134 @@ +import { Component, OnInit, AfterViewChecked } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthenticationService } from '../../services/authentication.service'; +import { UserService } from '../../services/user.service'; + +@Component({ + selector: 'app-presentation', + templateUrl: './presentation.component.html', + styleUrls: ['./presentation.component.css'] +}) + +export class PresentationComponent implements OnInit, AfterViewChecked { + + private email: string; + private password: string; + private confirmPassword: string; + private nickName: string; + private roleUserSignup = 'student'; + + private loginView = true; + private fieldsIncorrect: boolean; + private submitProcessing: boolean; + + // Error message content + private errorTitle: string; + private errorContent: string; + private customClass: string; + + constructor( + private authenticationService: AuthenticationService, + private userService: UserService, + private router: Router) { } + + ngOnInit() { } + + // If the user is loggedIn, navigates to dashboard + ngAfterViewChecked() { + if (this.authenticationService.isLoggedIn()) { + this.router.navigate(['/lessons']); + } + } + + setLoginView(option: boolean) { + this.fieldsIncorrect = false; + this.loginView = option; + } + + onSubmit() { + console.log('Submit: email = ' + this.email + ' , password = ' + this.password + ', confirmPassword = ' + this.confirmPassword); + this.submitProcessing = true; + + if (this.loginView) { + // If login view is activated + console.log('Logging in...'); + this.logIn(this.email, this.password); + } else { + // If signup view is activated + console.log('Signing up...'); + this.signUp(); + } + } + + logIn(user: string, pass: string) { + this.authenticationService.logIn(user, pass).subscribe( + result => { + this.submitProcessing = false; + + console.log('Login succesful! LOGGED AS ' + this.authenticationService.getCurrentUser().name); + + // Login successful + this.fieldsIncorrect = false; + this.router.navigate(['/lessons']); + }, + error => { + + console.log('Login failed (error): ' + error); + + this.errorTitle = 'Invalid field'; + this.errorContent = 'Please check your email or password'; + this.customClass = 'fail'; + + // Login failed + this.handleError(); + } + ); + } + + signUp() { + + // Passwords don't match + if (this.password !== this.confirmPassword) { + this.errorTitle = 'Your passwords don\'t match!'; + this.errorContent = ''; + this.customClass = 'fail'; + this.handleError(); + } + + else { + + let userNameFixed = this.email; + let userPasswordFixed = this.password; + + this.userService.newUser(userNameFixed, userPasswordFixed, this.nickName, this.roleUserSignup).subscribe( + result => { + + // Sign up succesful + this.logIn(userNameFixed, userPasswordFixed); + console.log('Sign up succesful!'); + }, + error => { + + console.log('Sign up failed (error): ' + error); + if (error === 409) { // CONFLICT: Email already in use + this.errorTitle = 'Invalid email'; + this.errorContent = 'That email is already in use'; + this.customClass = 'fail'; + } else if (error === 403) { // FORBIDDEN: Invalid email format + this.errorTitle = 'Invalid email format'; + this.errorContent = 'Our server has rejected that email'; + this.customClass = 'fail'; + } + + // Sign up failed + this.handleError(); + } + ); + } + } + + handleError() { + this.submitProcessing = false; + this.fieldsIncorrect = true; + } +} diff --git a/openvidu-sample-app/frontend/src/app/components/profile/profile.component.css b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.css new file mode 100644 index 00000000..cb6d69b4 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.css @@ -0,0 +1,9 @@ +table { + margin-top: 15px; + border-collapse: separate; + border-spacing: 15px 17px; +} + +th { + text-align: left; +} diff --git a/openvidu-sample-app/frontend/src/app/components/profile/profile.component.html b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.html new file mode 100644 index 00000000..b9f74e38 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.html @@ -0,0 +1,17 @@ +
+
+ +
MY PROFILE
+ + + + + + + + + +
Name{{authenticationService.getCurrentUser().nickName}}
Email{{authenticationService.getCurrentUser().name}}
+ +
+
diff --git a/openvidu-sample-app/frontend/src/app/components/profile/profile.component.ts b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.ts new file mode 100644 index 00000000..5243b6e1 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/profile/profile.component.ts @@ -0,0 +1,21 @@ +import { Component, OnInit } from '@angular/core'; + +import { AuthenticationService } from '../../services/authentication.service'; +import { User } from '../../models/user'; + +@Component({ + selector: 'app-profile', + templateUrl: './profile.component.html', + styleUrls: ['./profile.component.css'] +}) +export class ProfileComponent implements OnInit { + + private user: User; + + constructor(private authenticationService: AuthenticationService) { } + + ngOnInit() { + this.user = this.authenticationService.getCurrentUser(); + } + +} diff --git a/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.css b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.css new file mode 100644 index 00000000..1ba99145 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.css @@ -0,0 +1,36 @@ +h1 { + text-align: center; + margin: 0; + color: white; +} + +#header-div { + position: absolute; + z-index: 1000; + width: 100%; + background: rgba(0, 0, 0, 0.4); +} + +md-icon { + font-size: 38px; + width: 38px; + height: 38px; + color: white; + -webkit-transition: color .2s linear; + -moz-transition: color .2s linear; + -ms-transition: color .2s linear; + -o-transition: color .2s linear; + transition: color .2s linear; +} + +md-icon:hover { + color: #ffd740; +} + +#back-btn { + float: left; +} + +.right-btn { + float: right; +} diff --git a/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.html b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.html new file mode 100644 index 00000000..81b96291 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.html @@ -0,0 +1,9 @@ +
+ keyboard_arrow_left + {{fullscreenIcon}} + {{videoIcon}} + {{audioIcon}} +

{{lesson?.title}}

+
+
+
\ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.ts b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.ts new file mode 100644 index 00000000..29e07e3b --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/components/video-session/video-session.component.ts @@ -0,0 +1,252 @@ +import { Component, OnInit } from '@angular/core'; +import { Location } from '@angular/common'; +import { OpenVidu, Session, Stream } from 'openvidu-browser'; + +import { VideoSessionService } from '../../services/video-session.service'; +import { AuthenticationService } from '../../services/authentication.service'; + +import { Lesson } from '../../models/lesson'; + +@Component({ + selector: 'app-video-session', + templateUrl: './video-session.component.html', + styleUrls: ['./video-session.component.css'] +}) +export class VideoSessionComponent implements OnInit { + + lesson: Lesson; + + OV: OpenVidu; + session: Session; + + sessionId: string; + token: string; + + cameraOptions: any; + localParentId: string = 'local-stream'; + remoteParentId: string = 'remote-streams'; + + localVideoActivated: boolean; + localAudioActivated: boolean; + videoIcon: string; + audioIcon: string; + fullscreenIcon: string; + + constructor( + private location: Location, + private authenticationService: AuthenticationService, + private videoSessionService: VideoSessionService) { } + + + OPEN_VIDU_CONNECTION() { + + // 0) Obtain 'sessionId' and 'token' from server + // In this case, the method ngOnInit takes care of it + + // 1) Initialize OpenVidu and your Session + this.OV = new OpenVidu("wss://" + location.hostname + ":8443/"); + this.session = this.OV.initSession(this.sessionId); + + + // 2) Specify the actions when participants enter and leave the session + this.session.onStreamAddedOV((stream) => { + console.warn("Stream added:"); + console.warn(stream); + stream.playOnlyVideo(this.remoteParentId, null); + }); + this.session.onStreamRemovedOV((stream) => { + console.warn("Stream removed:"); + console.warn(stream); + stream.removeVideo(this.remoteParentId); + }); + this.session.onParticipantJoinedOV((participant) => { + console.warn("Participant joined:"); + console.warn(participant); + }); + this.session.onParticipantLeftOV((participant) => { + console.warn("Participant left:"); + console.warn(participant); + }); + this.session.onParticipantPublishedOV((participant) => { + console.warn("Participant published:"); + console.warn(participant); + }); + this.session.onParticipantEvictedOV((participant) => { + console.warn("Participant evicted:"); + console.warn(participant); + }); + this.session.onRoomClosedOV((room) => { + console.warn("Room closed:"); + console.warn(room); + }); + this.session.onLostConnectionOV((room) => { + console.warn("Connection lost:"); + console.warn(room); + }); + this.session.onMediaErrorOV((error) => { + console.warn("Media error:"); + console.warn(error); + }); + + + // 3) Connect to the session + this.session.connect(this.token, (error) => { + if (error) return console.log("There was an error: " + error); + + + // 4) Get your own camera stream with the desired resolution and publish it, only if the user is supposed to do so + + // Local publish generating an HTML video element as a child of parentId HTML element + this.OV.initPublisherTagged(this.localParentId, this.cameraOptions, (error) => { + if (error) return console.log("There was an error with your camera: " + error); + this.session.publish(); + }); + + // Local publish without generating an HTML video element + /*this.OV.initPublisher(this.cameraOptions, (error) => { + if (error) return console.log("There was an error: " + error); + this.session.publish(); + });*/ + + }); + + } + + + ngOnInit() { + + // Specific aspects of this concrete application + this.previousConnectionStuff(); + + + if (this.authenticationService.isTeacher()) { + + // If the user is the teacher: creates the session and gets a token (with PUBLISHER role) + this.videoSessionService.createSession(this.lesson.id).subscribe( + sessionId => { + this.sessionId = sessionId; + this.videoSessionService.generateToken(this.lesson.id).subscribe( + sessionIdAndToken => { + this.token = sessionIdAndToken[1]; + console.warn("Token: " + this.token); + console.warn("SessionId: " + this.sessionId); + this.OPEN_VIDU_CONNECTION(); + }, + error => { + console.log(error); + }); + }, + error => { + console.log(error); + } + ); + } + else { + + // If the user is a student: gets a token (with SUBSCRIBER role) + this.videoSessionService.generateToken(this.lesson.id).subscribe( + sessionIdAndToken => { + this.sessionId = sessionIdAndToken[0]; + this.token = sessionIdAndToken[1]; + console.warn("Token: " + this.token); + console.warn("SessionId: " + this.sessionId); + this.OPEN_VIDU_CONNECTION(); + }, + error => { + console.log(error); + }); + } + + + // Specific aspects of this concrete application + this.afterConnectionStuff(); + } + + ngAfterViewInit() { + this.toggleScrollPage("hidden"); + } + + ngOnDestroy() { + this.toggleScrollPage("auto"); + this.exitFullScreen(); + if (this.OV) this.OV.close(false); + } + + toggleLocalVideo() { + this.localVideoActivated = !this.localVideoActivated; + this.OV.toggleLocalVideoTrack(this.localVideoActivated); + this.videoIcon = this.localVideoActivated ? 'videocam' : 'videocam_off'; + } + + toggleLocalAudio() { + this.localAudioActivated = !this.localAudioActivated; + this.OV.toggleLocalAudioTrack(this.localAudioActivated); + this.audioIcon = this.localAudioActivated ? 'mic' : 'mic_off'; + } + + toggleScrollPage(scroll: string) { + let content = document.getElementsByClassName("mat-sidenav-content")[0]; + content.style.overflow = scroll; + } + + toggleFullScreen() { + let document: any = window.document; + let fs = document.getElementsByTagName('html')[0]; + if (!document.fullscreenElement && + !document.mozFullScreenElement && + !document.webkitFullscreenElement && + !document.msFullscreenElement) { + console.log("enter FULLSCREEN!"); + this.fullscreenIcon = 'fullscreen_exit'; + if (fs.requestFullscreen) { + fs.requestFullscreen(); + } else if (fs.msRequestFullscreen) { + fs.msRequestFullscreen(); + } else if (fs.mozRequestFullScreen) { + fs.mozRequestFullScreen(); + } else if (fs.webkitRequestFullscreen) { + fs.webkitRequestFullscreen(); + } + } else { + console.log("exit FULLSCREEN!"); + this.fullscreenIcon = 'fullscreen'; + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } + } + + exitFullScreen() { + let document: any = window.document; + let fs = document.getElementsByTagName('html')[0]; + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } + } + + previousConnectionStuff() { + this.lesson = this.videoSessionService.lesson; + this.cameraOptions = this.videoSessionService.cameraOptions; + } + + afterConnectionStuff() { + this.localVideoActivated = this.cameraOptions.video; + this.localAudioActivated = this.cameraOptions.audio; + this.videoIcon = this.localVideoActivated ? "videocam" : "videocam_off"; + this.audioIcon = this.localAudioActivated ? "mic" : "mic_off"; + this.fullscreenIcon = "fullscreen"; + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/app/models/lesson.ts b/openvidu-sample-app/frontend/src/app/models/lesson.ts new file mode 100644 index 00000000..464e2360 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/models/lesson.ts @@ -0,0 +1,15 @@ +import { User } from './user'; + +export class Lesson { + + public id?: number; + public title: string; + public teacher: User; + public attenders: User[]; + + constructor(title: string) { + this.title = title; + this.attenders = []; + } + +} diff --git a/openvidu-sample-app/frontend/src/app/models/user.ts b/openvidu-sample-app/frontend/src/app/models/user.ts new file mode 100644 index 00000000..f5f06efd --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/models/user.ts @@ -0,0 +1,19 @@ +import { Lesson } from './lesson'; + +export class User { + + public id?: number; + public name: string; + public nickName: string; + public roles: string[]; + public lessons: Lesson[]; + + constructor(u: User) { + this.id = u.id; + this.name = u.name; + this.nickName = u.nickName; + this.roles = u.roles; + this.lessons = []; + } + +} diff --git a/openvidu-sample-app/frontend/src/app/services/authentication.service.ts b/openvidu-sample-app/frontend/src/app/services/authentication.service.ts new file mode 100644 index 00000000..c3674d16 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/services/authentication.service.ts @@ -0,0 +1,147 @@ +import { Observable } from 'rxjs/Rx'; +import { Injectable } from '@angular/core'; +import { Http, RequestOptions, Headers, Response } from '@angular/http'; +import { Router } from '@angular/router'; + +import { environment } from '../../environments/environment'; + +import 'rxjs/add/operator/map'; + +import { User } from '../models/user'; + +@Injectable() +export class AuthenticationService { + + private urlLogIn = environment.URL_BACK + '/api-logIn'; + private urlLogOut = environment.URL_BACK + '/api-logOut'; + + public token: string; + private user: User; + private role: string; + + constructor(private http: Http, private router: Router) { + this.reqIsLogged(); + + // set token if saved in local storage + // let auth_token = JSON.parse(localStorage.getItem('auth_token')); + // this.token = auth_token && auth_token.token; + } + + logIn(user: string, pass: string) { + + console.log('Login service started...'); + + let userPass = utf8_to_b64(user + ':' + pass); + let headers = new Headers({ + 'Authorization': 'Basic ' + userPass, + 'X-Requested-With': 'XMLHttpRequest' + }); + let options = new RequestOptions({ headers }); + + return this.http.get(this.urlLogIn, options) + .map(response => { + this.processLogInResponse(response); + return this.user; + }) + .catch(error => Observable.throw(error)); + } + + logOut() { + + console.log('Logging out...'); + + return this.http.get(this.urlLogOut).map( + response => { + + console.log('Logout succesful!'); + + this.user = null; + this.role = null; + + // clear token remove user from local storage to log user out and navigates to welcome page + this.token = null; + localStorage.removeItem('login'); + localStorage.removeItem('rol'); + this.router.navigate(['']); + + return response; + }) + .catch(error => Observable.throw(error)); + } + + directLogOut() { + this.logOut().subscribe( + response => { }, + error => console.log("Error when trying to log out: " + error) + ); + } + + private processLogInResponse(response) { + // Correctly logged in + console.log('Login succesful processing...'); + + this.user = (response.json() as User); + + localStorage.setItem('login', 'OPENVIDUAPP'); + if (this.user.roles.indexOf('ROLE_TEACHER') !== -1) { + this.role = 'ROLE_TEACHER'; + localStorage.setItem('rol', 'ROLE_TEACHER'); + } + if (this.user.roles.indexOf('ROLE_STUDENT') !== -1) { + this.role = 'ROLE_STUDENT'; + localStorage.setItem('rol', 'ROLE_STUDENT'); + } + } + + reqIsLogged() { + + console.log('ReqIsLogged called'); + + let headers = new Headers({ + 'X-Requested-With': 'XMLHttpRequest' + }); + let options = new RequestOptions({ headers }); + + this.http.get(this.urlLogIn, options).subscribe( + response => this.processLogInResponse(response), + error => { + if (error.status != 401) { + console.error('Error when asking if logged: ' + JSON.stringify(error)); + this.logOut(); + } + } + ); + } + + checkCredentials() { + if (!this.isLoggedIn()) { + this.logOut(); + } + } + + isLoggedIn() { + return ((this.user != null) && (this.user !== undefined)); + } + + getCurrentUser() { + return this.user; + } + + isTeacher() { + return ((this.user.roles.indexOf('ROLE_TEACHER')) !== -1) && (localStorage.getItem('rol') === 'ROLE_TEACHER'); + } + + isStudent() { + return ((this.user.roles.indexOf('ROLE_STUDENT')) !== -1) && (localStorage.getItem('rol') === 'ROLE_STUDENT'); + } + + updateUserLessons(lessons) { + this.getCurrentUser().lessons = lessons; + } +} + +function utf8_to_b64(str) { + return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { + return String.fromCharCode('0x' + p1); + })); +} diff --git a/openvidu-sample-app/frontend/src/app/services/lesson.service.ts b/openvidu-sample-app/frontend/src/app/services/lesson.service.ts new file mode 100644 index 00000000..e84d32c6 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/services/lesson.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers, RequestOptions, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; +import { environment } from '../../environments/environment'; + +import { Lesson } from '../models/lesson'; +import { User } from '../models/user'; +import { AuthenticationService } from './authentication.service'; + +import 'rxjs/Rx'; + +@Injectable() +export class LessonService { + + private url = environment.URL_BACK + '/api-lessons'; + + constructor(private http: Http, private authenticationService: AuthenticationService) { } + + getLessons(user: User) { + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.get(this.url + '/user/' + user.id, options) // Must send userId + .map((response: Response) => response.json() as Lesson[]) + .catch(error => this.handleError(error)); + } + + getLesson(lessonId: number) { + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.get(this.url + '/lesson/' + lessonId, options) // Must send userId + .map((response: Response) => response.json() as Lesson) + .catch(error => this.handleError(error)); + } + + // POST new lesson. On success returns the created lesson + newLesson(lesson: Lesson) { + let body = JSON.stringify(lesson); + let headers = new Headers({ + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }); + let options = new RequestOptions({ headers }); + return this.http.post(this.url + '/new', body, options) + .map(response => response.json() as Lesson) + .catch(error => this.handleError(error)); + } + + // PUT existing lesson. On success returns the updated lesson + editLesson(lesson: Lesson) { + let body = JSON.stringify(lesson); + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.put(this.url + '/edit', body, options) + .map(response => response.json() as Lesson) + .catch(error => this.handleError(error)); + } + + // DELETE existing lesson. On success returns the deleted lesson (simplified version) + deleteLesson(lessonId: number) { + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.delete(this.url + '/delete/' + lessonId, options) + .map(response => response.json() as Lesson) + .catch(error => this.handleError(error)); + } + + // PUT existing lesson, modifying its attenders (adding them). On success returns the updated lesson.attenders array + addLessonAttenders(lessonId: number, userEmails: string[]) { + let body = JSON.stringify(userEmails); + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.put(this.url + '/edit/add-attenders/lesson/' + lessonId, body, options) + .map(response => response.json()) + .catch(error => this.handleError(error)); + } + + // PUT existing lesson, modifying its attenders (deleting them). On success returns the updated lesson.attenders array + deleteLessonAttenders(lesson: Lesson) { + let body = JSON.stringify(lesson); + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.put(this.url + '/edit/delete-attenders', body, options) + .map(response => response.json() as User[]) + .catch(error => this.handleError(error)); + } + + obtainLocalLesson(id: number) { + return this.authenticationService.getCurrentUser().lessons.find(lesson => lesson.id == id); + } + + private handleError(error: any) { + console.error(error); + return Observable.throw('Server error (' + error.status + '): ' + error.text()) + } +} diff --git a/openvidu-sample-app/frontend/src/app/services/user.service.ts b/openvidu-sample-app/frontend/src/app/services/user.service.ts new file mode 100644 index 00000000..84baf9da --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/services/user.service.ts @@ -0,0 +1,29 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers, RequestOptions, Response } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { User } from '../models/user'; + +@Injectable() +export class UserService { + + private url = '/api-users'; + + constructor(private http: Http) { } + + newUser(name: string, pass: string, nickName: string, role: string) { + let body = JSON.stringify([name, pass, nickName, role]); + let headers = new Headers({ + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }); + let options = new RequestOptions({ headers }); + return this.http.post(this.url + "/new", body, options) + .map(response => response.json() as User) + .catch(error => this.handleError(error)); + } + + private handleError(error: any) { + return Observable.throw(error.status); + } +} diff --git a/openvidu-sample-app/frontend/src/app/services/video-session.service.ts b/openvidu-sample-app/frontend/src/app/services/video-session.service.ts new file mode 100644 index 00000000..58d9fd24 --- /dev/null +++ b/openvidu-sample-app/frontend/src/app/services/video-session.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { Http, Headers, RequestOptions } from '@angular/http'; +import { Observable } from 'rxjs/Observable'; + +import { Lesson } from '../models/lesson'; + +import { AuthenticationService } from './authentication.service'; + +@Injectable() +export class VideoSessionService { + + lesson: Lesson; + cameraOptions: any; + + constructor(private http: Http, private authenticationService: AuthenticationService) { } + + // Returns "sessionId" + createSession(lessonId: number) { + let body = JSON.stringify(lessonId); + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.post('/api-sessions/create-session', body, options) + .map(response => response.text()) + .catch(error => this.handleError(error)); + } + + // Returns {0: sessionId, 1: token} + generateToken(lessonId: number) { + let body = JSON.stringify(lessonId); + let headers = new Headers({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + this.authenticationService.token }); + let options = new RequestOptions({ headers }); + return this.http.post('/api-sessions/generate-token', body, options) + .map(response => response.json()) + .catch(error => this.handleError(error)); + } + + private handleError(error: any) { + console.error(error); + return Observable.throw('Server error (' + error.status + '): ' + error.text()) + } + +} \ No newline at end of file diff --git a/openvidu-sample-app/frontend/src/assets/.gitkeep b/openvidu-sample-app/frontend/src/assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/openvidu-sample-app/frontend/src/environments/environment.prod.ts b/openvidu-sample-app/frontend/src/environments/environment.prod.ts new file mode 100644 index 00000000..3612073b --- /dev/null +++ b/openvidu-sample-app/frontend/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/openvidu-sample-app/frontend/src/environments/environment.ts b/openvidu-sample-app/frontend/src/environments/environment.ts new file mode 100644 index 00000000..c8db4711 --- /dev/null +++ b/openvidu-sample-app/frontend/src/environments/environment.ts @@ -0,0 +1,9 @@ +// The file contents for the current environment will overwrite these during build. +// The build system defaults to the dev environment which uses `environment.ts`, but if you do +// `ng build --env=prod` then `environment.prod.ts` will be used instead. +// The list of which env maps to which file can be found in `.angular-cli.json`. + +export const environment = { + production: false, + URL_BACK: 'http://localhost:5000' +}; diff --git a/openvidu-sample-app/frontend/src/favicon.ico b/openvidu-sample-app/frontend/src/favicon.ico new file mode 100644 index 00000000..8081c7ce Binary files /dev/null and b/openvidu-sample-app/frontend/src/favicon.ico differ diff --git a/openvidu-sample-app/frontend/src/index.html b/openvidu-sample-app/frontend/src/index.html new file mode 100644 index 00000000..9cb9b834 --- /dev/null +++ b/openvidu-sample-app/frontend/src/index.html @@ -0,0 +1,23 @@ + + + + + + OpenviduSampleApp + + + + + + + + + + +
+
+
+
+ + + diff --git a/openvidu-sample-app/frontend/src/main.ts b/openvidu-sample-app/frontend/src/main.ts new file mode 100644 index 00000000..a9ca1caf --- /dev/null +++ b/openvidu-sample-app/frontend/src/main.ts @@ -0,0 +1,11 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule); diff --git a/openvidu-sample-app/frontend/src/polyfills.ts b/openvidu-sample-app/frontend/src/polyfills.ts new file mode 100644 index 00000000..53bdaf1b --- /dev/null +++ b/openvidu-sample-app/frontend/src/polyfills.ts @@ -0,0 +1,68 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** IE9, IE10 and IE11 requires all of the following polyfills. **/ +// import 'core-js/es6/symbol'; +// import 'core-js/es6/object'; +// import 'core-js/es6/function'; +// import 'core-js/es6/parse-int'; +// import 'core-js/es6/parse-float'; +// import 'core-js/es6/number'; +// import 'core-js/es6/math'; +// import 'core-js/es6/string'; +// import 'core-js/es6/date'; +// import 'core-js/es6/array'; +// import 'core-js/es6/regexp'; +// import 'core-js/es6/map'; +// import 'core-js/es6/set'; + +/** IE10 and IE11 requires the following for NgClass support on SVG elements */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** IE10 and IE11 requires the following to support `@angular/animation`. */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + +/** Evergreen browsers require these. **/ +import 'core-js/es6/reflect'; +import 'core-js/es7/reflect'; + + +/** ALL Firefox browsers require the following to support `@angular/animation`. **/ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + + + +/*************************************************************************************************** + * Zone JS is required by Angular itself. + */ +import 'zone.js/dist/zone'; // Included with Angular CLI. + + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ + +/** + * Date, currency, decimal and percent pipes. + * Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10 + */ +// import 'intl'; // Run `npm install --save intl`. diff --git a/openvidu-sample-app/frontend/src/styles.css b/openvidu-sample-app/frontend/src/styles.css new file mode 100644 index 00000000..c7e0e13c --- /dev/null +++ b/openvidu-sample-app/frontend/src/styles.css @@ -0,0 +1,230 @@ +@import '~@angular/material/core/theming/prebuilt/deeppurple-amber.css'; + +* { + font-family: 'Exo 2', sans-serif; +} + +html, +body { + height: 100%; + margin: 0; + padding: 0; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0.05); +} + +main { + flex: 1; +} + +a { + color: #fff; + text-decoration: none; +} + +li { + list-style: none; +} + +md-icon { + cursor: pointer; +} + +md-icon:hover { + color: grey; +} + +.container { + padding: 25px; +} + +.mat-sidenav-content { + min-height: 100% !important; + display: flex !important; + flex-direction: column !important; +} + +.hover-link:hover { + color: #ffd740 !important; +} + +.feedback-list { + font-weight: 500; + margin-top: 5px; + margin-left: 2em; + display: list-item; +} + +.div-inner-main { + min-width: 30%; + margin-left: 20px; + margin-right: 20px; + margin-top: 50px; + margin-bottom: 50px; +} + +.block-btn { + display: inline-block; +} + +#local-stream { + position: absolute; + width: 100%; +} + +#local-stream video { + width: 100%; +} + +#remote-streams { + position: absolute; + bottom: 0; +} + +#remote-streams video { + width: 350px; +} + + +/* Colors of Deep Purple Material Theme */ + +.back-primary { + background: #673ab7 !important; +} + +.back-accent { + background: #ffd740; +} + +.back-warn { + background: #f44336; +} + +.back-secondary { + background: #D1C4E9; +} + +.color-primary { + color: #673ab7 !important; +} + +.color-accent { + color: #ffd740; +} + +.color-warn { + color: #f44336; +} + +.color-secondary { + color: #D1C4E9; +} + + +/*Loader*/ + +.cssload-container { + position: absolute; + top: 50%; + left: 50%; + z-index: 1000; + margin: -25px 0 0 -25px; + border-radius: 50%; + width: 50px; + height: 50px; +} + +.cssload-tube-tunnel { + width: 56px; + height: 56px; + margin: 0 auto; + border: 5px solid; + border-radius: 50%; + border-color: #673ab7; + animation: cssload-scale 1035ms infinite linear; + -o-animation: cssload-scale 1035ms infinite linear; + -ms-animation: cssload-scale 1035ms infinite linear; + -webkit-animation: cssload-scale 1035ms infinite linear; + -moz-animation: cssload-scale 1035ms infinite linear; +} + +@keyframes cssload-scale { + 0% { + transform: scale(0); + transform: scale(0); + } + 90% { + transform: scale(0.7); + transform: scale(0.7); + } + 100% { + transform: scale(1); + transform: scale(1); + } +} + +@-o-keyframes cssload-scale { + 0% { + -o-transform: scale(0); + transform: scale(0); + } + 90% { + -o-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -o-transform: scale(1); + transform: scale(1); + } +} + +@-ms-keyframes cssload-scale { + 0% { + -ms-transform: scale(0); + transform: scale(0); + } + 90% { + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -ms-transform: scale(1); + transform: scale(1); + } +} + +@-webkit-keyframes cssload-scale { + 0% { + -webkit-transform: scale(0); + transform: scale(0); + } + 90% { + -webkit-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@-moz-keyframes cssload-scale { + 0% { + -moz-transform: scale(0); + transform: scale(0); + } + 90% { + -moz-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -moz-transform: scale(1); + transform: scale(1); + } +} + +.filtered { + filter: blur(5px); +} + + +/*End Loader*/ diff --git a/openvidu-sample-app/frontend/src/test.ts b/openvidu-sample-app/frontend/src/test.ts new file mode 100644 index 00000000..9bf72267 --- /dev/null +++ b/openvidu-sample-app/frontend/src/test.ts @@ -0,0 +1,32 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/dist/long-stack-trace-zone'; +import 'zone.js/dist/proxy.js'; +import 'zone.js/dist/sync-test'; +import 'zone.js/dist/jasmine-patch'; +import 'zone.js/dist/async-test'; +import 'zone.js/dist/fake-async-test'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any. +declare var __karma__: any; +declare var require: any; + +// Prevent Karma from running prematurely. +__karma__.loaded = function () {}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); +// Finally, start Karma to run the tests. +__karma__.start(); diff --git a/openvidu-sample-app/frontend/src/tsconfig.app.json b/openvidu-sample-app/frontend/src/tsconfig.app.json new file mode 100644 index 00000000..9f12c4b8 --- /dev/null +++ b/openvidu-sample-app/frontend/src/tsconfig.app.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2016", + "dom" + ], + "outDir": "../out-tsc/app", + "target": "es5", + "module": "es2015", + "baseUrl": "", + "types": [] + }, + "exclude": [ + "test.ts", + "**/*.spec.ts" + ] +} diff --git a/openvidu-sample-app/frontend/src/tsconfig.spec.json b/openvidu-sample-app/frontend/src/tsconfig.spec.json new file mode 100644 index 00000000..6c5160e1 --- /dev/null +++ b/openvidu-sample-app/frontend/src/tsconfig.spec.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "lib": [ + "es2016" + ], + "outDir": "../out-tsc/spec", + "module": "commonjs", + "target": "es6", + "baseUrl": "", + "types": [ + "jasmine", + "node" + ] + }, + "files": [ + "test.ts" + ], + "include": [ + "**/*.spec.ts" + ] +} diff --git a/openvidu-sample-app/frontend/src/typings.d.ts b/openvidu-sample-app/frontend/src/typings.d.ts new file mode 100644 index 00000000..edc2f1e5 --- /dev/null +++ b/openvidu-sample-app/frontend/src/typings.d.ts @@ -0,0 +1,4 @@ +/* SystemJS module definition */ +declare var module: { + id: string; +}; diff --git a/openvidu-sample-app/frontend/tsconfig.json b/openvidu-sample-app/frontend/tsconfig.json new file mode 100644 index 00000000..7a9d9e8c --- /dev/null +++ b/openvidu-sample-app/frontend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "outDir": "./dist/out-tsc", + "sourceMap": true, + "declaration": false, + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es5", + "lib": [ + "es2016", + "dom" + ] + } +} diff --git a/openvidu-sample-app/frontend/tslint.json b/openvidu-sample-app/frontend/tslint.json new file mode 100644 index 00000000..9113f136 --- /dev/null +++ b/openvidu-sample-app/frontend/tslint.json @@ -0,0 +1,116 @@ +{ + "rulesDirectory": [ + "node_modules/codelyzer" + ], + "rules": { + "callable-types": true, + "class-name": true, + "comment-format": [ + true, + "check-space" + ], + "curly": true, + "eofline": true, + "forin": true, + "import-blacklist": [true, "rxjs"], + "import-spacing": true, + "indent": [ + true, + "spaces" + ], + "interface-over-type-literal": true, + "label-position": true, + "max-line-length": [ + true, + 140 + ], + "member-access": false, + "member-ordering": [ + true, + "static-before-instance", + "variables-before-functions" + ], + "no-arg": true, + "no-bitwise": true, + "no-console": [ + true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-empty-interface": true, + "no-eval": true, + "no-inferrable-types": [true, "ignore-params"], + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-use-before-declare": true, + "no-var-keyword": true, + "object-literal-sort-keys": false, + "one-line": [ + true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "prefer-const": true, + "quotemark": [ + true, + "single" + ], + "radix": true, + "semicolon": [ + "always" + ], + "triple-equals": [ + true, + "allow-null-check" + ], + "typedef-whitespace": [ + true, + { + "call-signature": "nospace", + "index-signature": "nospace", + "parameter": "nospace", + "property-declaration": "nospace", + "variable-declaration": "nospace" + } + ], + "typeof-compare": true, + "unified-signatures": true, + "variable-name": false, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ], + + "directive-selector": [true, "attribute", "app", "camelCase"], + "component-selector": [true, "element", "app", "kebab-case"], + "use-input-property-decorator": true, + "use-output-property-decorator": true, + "use-host-property-decorator": true, + "no-input-rename": true, + "no-output-rename": true, + "use-life-cycle-interface": true, + "use-pipe-transform-interface": true, + "component-class-suffix": true, + "directive-class-suffix": true, + "no-access-missing-member": true, + "templates-use-public": true, + "invoke-injectable": true + } +}