Skip to content

Commit

Permalink
Merge pull request #350 from jphacks/develop
Browse files Browse the repository at this point in the history
release 1.1.0
  • Loading branch information
averak authored Nov 13, 2022
2 parents 3613c7f + 89a8f64 commit fde6bec
Show file tree
Hide file tree
Showing 356 changed files with 60,431 additions and 16,488 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
cache-dependency-path: ./desktop/yarn.lock
cache: npm
cache-dependency-path: ./desktop/package-lock.json

- name: dependencies
run: yarn install
run: npm ci

- name: code check
run: |
yarn code-check
npm run code-check
- name: build
run: |
yarn build
npm run build
pointer:
runs-on: ubuntu-latest
Expand All @@ -73,16 +73,16 @@ jobs:
uses: actions/setup-node@v3
with:
node-version: 16
cache: yarn
cache-dependency-path: ./app/src/main/pointer/yarn.lock
cache: npm
cache-dependency-path: ./app/src/main/pointer/package-lock.json

- name: dependencies
run: yarn install
run: npm install

- name: code check
run: |
yarn code-check
npm run code-check
- name: build
run: |
yarn build
npm run build
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,12 @@ local.properties
out/

### Application property ###
app/src/main/resources/application-local.yml
app/src/main/resources/application-dev.yml
app/src/main/resources/application-stage.yml
app/src/main/resources/application-prod.yml
app/src/main/resources/credentials/**/*
!app/src/main/resources/credentials/.gitkeep

### Static contents ###
app/src/main/resources/static/*
Expand Down
8 changes: 4 additions & 4 deletions Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pipeline {
}
steps {
dir("app/src/main/pointer") {
sh "yarn install"
sh "yarn build"
sh "npm ci"
sh "npm run build"
}
}
}
Expand All @@ -42,7 +42,7 @@ pipeline {
stage("deploy") {
steps {
sh "docker-compose up -d"
sh "docker run --rm -d -v $PWD/app/build/libs:/app -p ${PORT}:8080 openjdk:11-jdk java -jar -Dspring.profiles.active=${ENVIRONMENT} app/smartpointer_1.0.0_SNAPSHOT.jar"
sh "docker run --rm -d -v $PWD/app/build/libs:/app -p ${PORT}:8080 openjdk:11-jdk java -jar -Dspring.profiles.active=${ENVIRONMENT} app/smartpointer_1.1.0.jar"
}
}
}
Expand All @@ -55,4 +55,4 @@ pipeline {
webhookURL: DISCORD_WEBHOOK_URL
}
}
}
}
85 changes: 41 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

![CI](https://github.com/jphacks/D_2208/workflows/CI/badge.svg)
![deploy](https://github.com/jphacks/D_2208/workflows/deploy/badge.svg)
![version](https://img.shields.io/badge/version-1.0.0-blue.svg)
![version](https://img.shields.io/badge/version-1.1.0-blue.svg)

[![IMAGE ALT TEXT HERE](https://jphacks.com/wp-content/uploads/2022/08/JPHACKS2022_ogp.jpg)](https://youtu.be/6NNR210ilGI)

Expand All @@ -16,36 +16,39 @@

> **Note**
>
> 詳細は[PRD (プロダクト要求仕様書)](https://github.com/jphacks/D_2208/wiki/PRD%20(プロダクト要求仕様書))を参照してください
> 詳細は[PRD (プロダクト要求仕様書)](<https://github.com/jphacks/D_2208/wiki/PRD%20(プロダクト要求仕様書)>)を参照してください
### 背景(製品開発のきっかけ、課題等)

複数人でプレゼンテーションを行う場合、スライドの切り替え担当者とスピーカーが一致しない場合があります。
しかし、スピーカーが逐一「次のスライドお願いします」と依頼するのは面倒であり、聴衆に煩わしさを与える場合もあるでしょう。

そこで、手元のスマートフォンをリモコン化し、リモートにあるホストPCのスライドを共同で操作できるプラットフォームを開発しました
そこで、手元のスマートフォンをリモコン化し、リモートにあるホスト PC のスライドを共同で操作できるプラットフォームを開発しました

### 製品説明(具体的な製品の説明)

* ルームの管理
* スライド投影するホストPCでルームを作成できる
* 招待リンクを発行し、共同プレゼンターに共有できる
* 発表が終わったら、ルームを終了できる
* カスタムポインターを作成できる
* ポインタータイプを変更できる
* リモコン操作
* 招待リンクからルームに参加できる
* 表示名を設定できる
* タイマーを設定できる
* 残り時間通知を設定できる
* スライドを進める/戻すことができる
* スライド上にポインターを表示できる
* ポインタータイプを変更できる
* 上記操作は共同プレゼンター全員に共有される

### 特長

#### 1. リモートからでも共同でスライド操作できる

ホストPCに投影されるスライドを、共同プレゼンター全員が操作可能になります。
ホストPCはルームをホスティングする以外は操作不要で、スマートフォンからのスライド切り替えで発表を行います。
ホスト PC に投影されるスライドを、共同プレゼンター全員が操作可能になります。
ホスト PC はルームをホスティングする以外は操作不要で、スマートフォンからのスライド切り替えで発表を行います。

#### 2. 発表をもっと快適に

Expand All @@ -60,39 +63,40 @@

### 解決出来ること

類似した既存のサービスは、自分のPCを自分のスマートフォンから操作するところまでしか実現できていません
スマートポインターを使えば、 背景説明はAさん、技術説明はBさんが発表担当といったプランの場合に「次のスライドお願いします」とスライド担当者に依頼する必要がなくなります。
類似した既存のサービスは、自分の PC を自分のスマートフォンから操作するところまでしか実現できていません
スマートポインターを使えば、 背景説明は A さん、技術説明は B さんが発表担当といったプランの場合に「次のスライドお願いします」とスライド担当者に依頼する必要がなくなります。

### 今後の展望

* スライドのプレビュー表示
* スライドの進める/戻すボタンにプレビュー表示するとより便利になる
* ユーザごとにレーザーポインターを表示
* 現在は一つのレーザーポインターを全員でシェアしている
* レーザーポインターの隣にユーザ名が表示されるとGood
* パフォーマンスの改善
* STOMPという軽量なメッセージングプロトコルを採用することでパフォーマンス向上を図ったが、スマートフォン・PC間の通信時にサーバを経由するためややタイムラグが生じている
* ポインタ渡しでプレゼンター権限を譲渡
* 現在は全ルーム参加者が同時に操作可能になっている
* 操作できるユーザを制限して、自分の発表担当箇所が終わったら次の人にポインタ渡しできるとGood
* Web会議向けに拡張
* プレゼン以外の場でも使えるプラットフォームにしたい

### 注力したこと(こだわり等)

* タイマー設定やスライド切り替え依頼などをシステム側で担うことで、既存の共同プレゼンテーションにおける煩わしさを徹底的に排除した
* 想定されるシナリオを網羅した質の高いテスト
* Zoomの仮想背景のように、カスタムポインターを作成することで好きなポインターを表示できる

### 用語集

* ルーム
* 共同プレゼンターが参加し、スライドをリモコン操作するためのワークスペース
* ホストPC
* [デスクトップアプリケーション](./desktop)を実行する
* スライド投影するPC
* OSのメニューバーから操作可能
* リモコンアクションを購読し、パワーポイントなどのプレゼンテーションソフトウェアを操作する
* リモコン
* [リモコン用Webアプリ](./app/src/main/pointer)を実行する
* プレゼンテーション用のレーザーポインター的な概念
* 自分のスマートフォンをリモコン化し、リモートにあるホストPCのスライドを操作できる
* ポインタ渡し
* プレゼンター権限を譲渡する行為
- ルーム
- 共同プレゼンターが参加し、スライドをリモコン操作するためのワークスペース
- ホスト PC
- [デスクトップアプリケーション](./desktop)を実行する
- スライド投影する PC
- OS のメニューバーから操作可能
- リモコンアクションを購読し、パワーポイントなどのプレゼンテーションソフトウェアを操作する
- リモコン
- [リモコン用 Web アプリ](./app/src/main/pointer)を実行する
- プレゼンテーション用のレーザーポインター的な概念
- 自分のスマートフォンをリモコン化し、リモートにあるホスト PC のスライドを操作できる
- ポインタ渡し
- プレゼンター権限を譲渡する行為

## 開発技術

Expand All @@ -116,8 +120,7 @@
* Java OpenJDK 11
* Spock
* MySQL 8.0
* OpenAPI
* AsyncAPI
* GraphQL
* インフラ
* Google App Engine
* Google Cloud SQL
Expand All @@ -127,30 +130,24 @@
* GitHub Actions
* Jenkins

#### 通信プロトコル

* HTTP
* ルームの作成、入室機能をREST APIとして提供します
* OpenAPIによるスキーマ駆動開発を採用
* APIドキュメントは[Swagger UI](https://smartpointer.abelab.dev/swagger-ui/index.html)を参照してください
* STOMP(over WebSocket)
* 軽量なメッセージングプロトコル
* AsyncAPIによるスキーマ駆動開発を採用
* AsyncAPIは発展途上であり、コード・ドキュメント生成ツールが貧弱だった
* APIドキュメントは、[asyncapi.yml](./asyncapi.yml)[AsyncAPI Studio](https://studio.asyncapi.com/)にペーストすることで閲覧できます

#### アーキテクチャ

リモコン操作を行うモバイル端末と、ルームをホスティングするデスクトップが、APIサーバを介してSTOMP(over WebSocket)で通信します。
リモコン操作を行うモバイル端末と、ルームをホスティングするデスクトップが、APIサーバを介してGraphQL(over WebSocket)で通信します。

スライドのページ遷移、ポインター表示など、モバイル端末からの操作命令をデスクトップアプリケーションが購読し、パワーポイントなどのプレゼンテーションソフトウェアを操作することでリモート&複数人での共同プレゼンテーションを実現しています。

![](https://user-images.githubusercontent.com/50389029/196952980-36fa7fbd-4b96-4f4d-8f92-a0a41eeb0d7f.png)
![](https://user-images.githubusercontent.com/50389029/201505483-aab67ff9-f751-4409-9fe6-ef66949da45c.png)

### 独自技術

#### ハッカソンで開発した独自機能・技術

* ルーム・ユーザ情報を管理するREST API([Swagger UI](https://smartpointer.abelab.dev/swagger-ui/index.html)
* ルーム入室APIから返されたIDトークンを使って、STOMPのトピックをPublish/Subscribeする
* STOMPによる双方向通信API([asyncapi.yml](./asyncapi.yml)
* GraphQLによる双方向通信API
* デスクトップのスライド上にポインタ表示するOverlay window







21 changes: 12 additions & 9 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import org.yaml.snakeyaml.Yaml

buildscript {
ext {
SPRING_BOOT_VERSION = "2.7.1"
SPRING_BOOT_VERSION = "2.7.5"
}
repositories {
mavenCentral()
Expand Down Expand Up @@ -74,17 +74,22 @@ ext {
dependencies {
// spring boot
implementation "org.springframework.boot:spring-boot-starter-web:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-webflux:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-websocket:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-validation:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-security:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-graphql:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-starter-actuator:${SPRING_BOOT_VERSION}"
implementation "org.springframework.boot:spring-boot-devtools:${SPRING_BOOT_VERSION}"
testImplementation "org.springframework.boot:spring-boot-starter-test:${SPRING_BOOT_VERSION}"
testImplementation "org.springframework.graphql:spring-graphql-test:1.0.2"
testImplementation "io.projectreactor:reactor-test:3.5.0"
annotationProcessor "org.springframework.boot:spring-boot-configuration-processor:${SPRING_BOOT_VERSION}"

// spring security
implementation "org.springframework.security:spring-security-core:5.7.2"
implementation "org.springframework.security:spring-security-web:5.7.2"
implementation "org.springframework.security:spring-security-config:5.7.2"
// implementation "org.springframework.security:spring-security-messaging:5.7.2"
testImplementation "org.springframework.security:spring-security-test:5.7.2"

// spring session
Expand All @@ -110,13 +115,8 @@ dependencies {
// mysql
implementation "mysql:mysql-connector-java:8.0.29"

// swagger
implementation "io.springfox:springfox-boot-starter:3.0.0"
implementation "org.springdoc:springdoc-openapi-ui:1.6.9"
implementation "org.springdoc:springdoc-openapi-javadoc:1.6.9"
implementation "org.springdoc:springdoc-openapi-security:1.6.9"
implementation "com.github.therapi:therapi-runtime-javadoc:0.13.0"
annotationProcessor "com.github.therapi:therapi-runtime-javadoc-scribe:0.13.0"
// google cloud
implementation "com.google.cloud:google-cloud-storage:2.11.3"

// test
testImplementation "org.spockframework:spock-core:2.2-M1-groovy-4.0"
Expand All @@ -136,6 +136,9 @@ dependencies {
implementation "commons-net:commons-net:3.8.0"
implementation "commons-validator:commons-validator:1.7"
implementation "net.rakugakibox.util:yaml-resource-bundle:1.2"

// support greater than java 11
implementation "javax.xml.bind:jaxb-api:2.3.1"
}

tasks.withType(Test) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@SpringBootApplication
public class SmartPointerApplication {

public static void main(String[] args) {
public static void main(final String[] args) {
SpringApplication.run(SmartPointerApplication.class, args);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package dev.abelab.smartpointer.auth;

import java.io.IOException;
import java.util.Optional;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.http.HttpHeaders;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

import dev.abelab.smartpointer.property.AuthProperty;
import io.jsonwebtoken.Jwts;

/**
* 認可フィルタ
*/
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

private final AuthProperty authProperty;

private final UserDetailsService userDetailsService;

public JWTAuthorizationFilter(final AuthenticationManager authenticationManager, final AuthProperty authProperty,
final UserDetailsService userDetailsService) {
super(authenticationManager);
this.authProperty = authProperty;
this.userDetailsService = userDetailsService;
}

@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain chain)
throws IOException, ServletException {
final var authentication = this.getAuthentication(request);
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
}

private UsernamePasswordAuthenticationToken getAuthentication(final HttpServletRequest request) {
final var authorizationHeader = Optional.ofNullable(request.getHeader(HttpHeaders.AUTHORIZATION)).orElse("");
if (!authorizationHeader.startsWith("Bearer ")) {
return null;
}

try {
final var jwt = authorizationHeader.replace("Bearer ", "");
final var subject = Jwts.parser() //
.setSigningKey(this.authProperty.getJwt().getSecret().getBytes()) //
.requireIssuer(this.authProperty.getJwt().getIssuer()) //
.parseClaimsJws(jwt) //
.getBody() //
.getSubject();
final var principal = this.userDetailsService.loadUserByUsername(subject);
return new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
} catch (final Exception e) {
return null;
}
}

}
Loading

0 comments on commit fde6bec

Please sign in to comment.