Skip to content

Commit ef4f96b

Browse files
committedAug 8, 2020
Initial app
1 parent 863d939 commit ef4f96b

15 files changed

+890
-47
lines changed
 

‎.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
22

3+
server/secret.json
4+
35
# dependencies
46
/node_modules
57
/.pnp

‎.prettierrc

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"semi": false
3+
}

‎keycode.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
var sys = Application("System Events");
2+
sys.keyCode(124);

‎package.json

+13-2
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,32 @@
22
"name": "mac-remote-control",
33
"version": "0.1.0",
44
"private": true,
5+
"proxy": "http://localhost:5000/",
56
"dependencies": {
67
"@testing-library/jest-dom": "^4.2.4",
78
"@testing-library/react": "^9.3.2",
89
"@testing-library/user-event": "^7.1.2",
10+
"@types/express": "^4.17.7",
911
"@types/jest": "^24.0.0",
1012
"@types/node": "^12.0.0",
1113
"@types/react": "^16.9.0",
1214
"@types/react-dom": "^16.9.0",
15+
"@types/use-persisted-state": "^0.3.0",
16+
"concurrently": "^5.3.0",
17+
"express": "^4.17.1",
18+
"jwt-simple": "^0.5.6",
19+
"nodemon": "^2.0.4",
1320
"react": "^16.13.1",
1421
"react-dom": "^16.13.1",
22+
"react-modal": "^3.11.2",
1523
"react-scripts": "3.4.1",
16-
"typescript": "~3.7.2"
24+
"typescript": "~3.7.2",
25+
"use-persisted-state": "^0.3.0"
1726
},
1827
"scripts": {
19-
"start": "react-scripts start",
28+
"start-server": "cd server && nodemon --exec ts-node server.ts",
29+
"start-react": "react-scripts start",
30+
"start": "concurrently \"yarn start-server\" \"yarn start-react\"",
2031
"build": "react-scripts build",
2132
"test": "react-scripts test",
2233
"eject": "react-scripts eject"

‎server/routes/postAction.ts

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Response, Request } from "express"
2+
import { exec } from "child_process"
3+
import jwt from "jwt-simple"
4+
5+
export function postAction(req: Request, res: Response) {
6+
let result = false
7+
const request = jwt.decode(req.body.request, "", true)
8+
const keycode = request.keycode
9+
if (keycode) {
10+
exec(
11+
`osascript -l JavaScript -e "Application('System Events').keyCode(${parseInt(
12+
keycode
13+
)})"`
14+
)
15+
res.json({ message: "Your keycode has been sent" })
16+
result = true
17+
console.log(req.body.keycode)
18+
}
19+
if (!result) res.json({ message: "noop" })
20+
}

‎server/server.ts

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import bodyParser from "body-parser"
2+
import express from "express"
3+
import { postAction } from "./routes/postAction"
4+
import jwt from "jwt-simple"
5+
import { Errors } from "../src/errors"
6+
import { secret } from "./secret.json"
7+
8+
const app = express()
9+
const port = process.env.PORT || 5000
10+
11+
const sessionCounter: { [key: string]: number } = {}
12+
13+
app.use(bodyParser.json())
14+
app.use(bodyParser.urlencoded({ extended: true }))
15+
16+
app.use((req, res, next) => {
17+
if (req.body && req.body.sessionId) {
18+
if (req.body.sessionId in sessionCounter) next()
19+
else {
20+
if (Object.keys(sessionCounter).length > 20)
21+
res.status(500).send({
22+
message:
23+
"Session limit exceeded, please restart server or manually purge sessions",
24+
code: Errors.SessionLimitExceeded,
25+
})
26+
else {
27+
sessionCounter[req.body.sessionId] = 0
28+
next()
29+
}
30+
}
31+
} else {
32+
res.status(403).send({
33+
message: "You are missing the sessionId in the body",
34+
code: Errors.MissingSessionId,
35+
})
36+
}
37+
})
38+
39+
app.use((req, res, next) => {
40+
if (req.body && req.body.request) {
41+
try {
42+
let payload = jwt.decode(req.body.request, secret, false, "HS256")
43+
if (payload.counter === sessionCounter[req.body.sessionId]) {
44+
sessionCounter[req.body.sessionId]++
45+
next()
46+
} else {
47+
res.status(403).send({
48+
message:
49+
"Sorry, you sent the wrong counter value, your current session counter is at " +
50+
sessionCounter[req.body.sessionId],
51+
code: Errors.InvalidCounterValue,
52+
counter: sessionCounter[req.body.sessionId],
53+
})
54+
}
55+
} catch (e) {
56+
res.status(403).send({
57+
message: "Could not verify the token.",
58+
code: Errors.FailedTokenVerify,
59+
})
60+
}
61+
} else {
62+
res.status(403).send({
63+
message:
64+
"You need to send a request in the body as a JWT verified with the agreed secret with HS256.",
65+
code: Errors.MissingRequestToken,
66+
})
67+
}
68+
})
69+
70+
// messages
71+
app.post("/api/action", postAction)
72+
73+
// tslint:disable-next-line:no-console
74+
app.listen(port, () => console.log(`Listening on port ${port}`))

‎server/tsconfig.json

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2019",
4+
"lib": ["dom", "dom.iterable", "esnext"],
5+
"allowJs": true,
6+
"skipLibCheck": true,
7+
"esModuleInterop": true,
8+
"allowSyntheticDefaultImports": true,
9+
"strict": true,
10+
"forceConsistentCasingInFileNames": true,
11+
"module": "commonjs",
12+
"moduleResolution": "node",
13+
"resolveJsonModule": true,
14+
"isolatedModules": true,
15+
"noEmit": true,
16+
"jsx": "react"
17+
},
18+
"include": ["src"]
19+
}

‎src/App.css

+39-2
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,47 @@
22
text-align: center;
33
}
44

5+
button {
6+
position: relative;
7+
display: flex;
8+
background: none;
9+
flex: 1;
10+
justify-content: center;
11+
align-items: center;
12+
color: white;
13+
border: none;
14+
border-radius: 10%;
15+
border: 10px solid rgba(0, 0, 0, 0);
16+
}
17+
button::before {
18+
content: "";
19+
border-radius: 3rem;
20+
position: absolute;
21+
top: 1rem;
22+
bottom: 1rem;
23+
left: 1rem;
24+
right: 1rem;
25+
background: rgba(255, 255, 255, 0.3);
26+
}
27+
528
.App-logo {
629
height: 40vmin;
30+
display: flex;
31+
flex-direction: column;
32+
justify-content: stretch;
33+
align-content: stretch;
734
pointer-events: none;
835
}
36+
.secret {
37+
display: flex;
38+
flex: 1;
39+
}
40+
.control {
41+
display: flex;
42+
flex-direction: row;
43+
justify-content: stretch;
44+
flex: 3;
45+
}
946

1047
@media (prefers-reduced-motion: no-preference) {
1148
.App-logo {
@@ -18,8 +55,8 @@
1855
min-height: 100vh;
1956
display: flex;
2057
flex-direction: column;
21-
align-items: center;
22-
justify-content: center;
58+
justify-content: stretch;
59+
align-content: stretch;
2360
font-size: calc(10px + 2vmin);
2461
color: white;
2562
}

‎src/App.test.tsx

-9
This file was deleted.

‎src/App.tsx

+91-17
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,100 @@
1-
import React from 'react';
2-
import logo from './logo.svg';
3-
import './App.css';
1+
import "./App.css"
2+
3+
import React, { useRef, useState } from "react"
4+
import createPersistedState from "use-persisted-state"
5+
import jwt from "jwt-simple"
6+
import { makeid } from "./util"
7+
import { Errors } from "./errors"
8+
9+
// import logo from "./logo.svg"
10+
const useSecretState = createPersistedState("secret")
11+
const useSessionState = createPersistedState("sessionId")
12+
const useSessionCounterState = createPersistedState("sessionCounter")
13+
14+
// so we don't generate new ids unnecessarily in every render
15+
const randomid = makeid(10)
16+
const increment = (x: number) => x + 1
417

518
function App() {
19+
const [secret, setSecret] = useSecretState<string>("")
20+
const [session] = useSessionState<string>(randomid)
21+
const [sessionCounter, setSessionCounter] = useSessionCounterState<number>(0)
22+
const [touch, setTouch] = useState<boolean>(false)
23+
24+
const inputRef = useRef<HTMLInputElement>()
25+
const keyCode = (code: number, fromTouch: boolean) => {
26+
if (fromTouch) setTouch(true)
27+
if (!fromTouch && touch) return
28+
fetch("/api/action", {
29+
method: "POST",
30+
body: JSON.stringify({
31+
request: jwt.encode(
32+
{ counter: sessionCounter, keycode: code },
33+
secret,
34+
"HS256"
35+
),
36+
sessionId: session,
37+
}),
38+
headers: {
39+
"Content-Type": "application/json",
40+
},
41+
})
42+
.then((response) => {
43+
if (response.status === 200) setSessionCounter(increment)
44+
if (!response.ok) {
45+
console.log(response)
46+
return response.json()
47+
}
48+
})
49+
.then((jsonErrorMessage: any) => {
50+
console.log(jsonErrorMessage)
51+
if (!jsonErrorMessage) return
52+
if (jsonErrorMessage?.code === Errors.InvalidCounterValue) {
53+
setSessionCounter(jsonErrorMessage.counter)
54+
}
55+
})
56+
}
657
return (
758
<div className="App">
859
<header className="App-header">
9-
<img src={logo} className="App-logo" alt="logo" />
10-
<p>
11-
Edit <code>src/App.tsx</code> and save to reload.
12-
</p>
13-
<a
14-
className="App-link"
15-
href="https://reactjs.org"
16-
target="_blank"
17-
rel="noopener noreferrer"
18-
>
19-
Learn React
20-
</a>
60+
<div className="secret">
61+
<input
62+
style={{ display: secret === "" ? "flex" : "none" }}
63+
type="text"
64+
ref={inputRef as any}
65+
></input>
66+
<button
67+
style={{ display: secret === "" ? "flex" : "none" }}
68+
onClick={() =>
69+
inputRef.current?.value && setSecret(inputRef.current.value)
70+
}
71+
>
72+
Save secret
73+
</button>
74+
<button
75+
style={{ display: secret !== "" ? "flex" : "none" }}
76+
onClick={() => setSecret("")}
77+
>
78+
Change secret
79+
</button>
80+
</div>
81+
<div className="control">
82+
<button
83+
onMouseDown={keyCode.bind(null, 123, false)}
84+
onTouchStart={keyCode.bind(null, 123, true)}
85+
>
86+
Left Key
87+
</button>
88+
<button
89+
onMouseDown={keyCode.bind(null, 124, false)}
90+
onTouchStart={keyCode.bind(null, 124, true)}
91+
>
92+
Right Key
93+
</button>
94+
</div>
2195
</header>
2296
</div>
23-
);
97+
)
2498
}
2599

26-
export default App;
100+
export default App

‎src/errors.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export enum Errors {
2+
SessionLimitExceeded = 0,
3+
MissingSessionId,
4+
InvalidCounterValue,
5+
FailedTokenVerify,
6+
MissingRequestToken,
7+
}

‎src/index.tsx

+8-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
import React from 'react';
2-
import ReactDOM from 'react-dom';
3-
import './index.css';
4-
import App from './App';
5-
import * as serviceWorker from './serviceWorker';
1+
import React from "react"
2+
import ReactDOM from "react-dom"
3+
import "./index.css"
4+
import App from "./App"
5+
import * as serviceWorker from "./serviceWorker"
66

77
ReactDOM.render(
88
<React.StrictMode>
99
<App />
1010
</React.StrictMode>,
11-
document.getElementById('root')
12-
);
11+
document.getElementById("root")
12+
)
1313

1414
// If you want your app to work offline and load faster, you can change
1515
// unregister() to register() below. Note this comes with some pitfalls.
1616
// Learn more about service workers: https://bit.ly/CRA-PWA
17-
serviceWorker.unregister();
17+
serviceWorker.unregister()

‎src/util.ts

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/** https://stackoverflow.com/a/1349426 */
2+
export function makeid(length: number) {
3+
var result = ""
4+
var characters =
5+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
6+
var charactersLength = characters.length
7+
for (var i = 0; i < length; i++) {
8+
result += characters.charAt(Math.floor(Math.random() * charactersLength))
9+
}
10+
return result
11+
}

‎tsconfig.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"compilerOptions": {
3-
"target": "es5",
3+
"target": "es2019",
44
"lib": [
55
"dom",
66
"dom.iterable",

‎yarn.lock

+600-8
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.