Websockets with auth pipeline!
This commit is contained in:
parent
36412c9f58
commit
a00fa5c194
86
client/package-lock.json
generated
86
client/package-lock.json
generated
@ -11,7 +11,8 @@
|
|||||||
"chota": "^0.9.2",
|
"chota": "^0.9.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.10.0"
|
"react-router-dom": "^6.10.0",
|
||||||
|
"socket.io-client": "^4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
@ -784,6 +785,11 @@
|
|||||||
"node": ">=14"
|
"node": ">=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@socket.io/component-emitter": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||||
|
},
|
||||||
"node_modules/@types/prop-types": {
|
"node_modules/@types/prop-types": {
|
||||||
"version": "15.7.5",
|
"version": "15.7.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz",
|
||||||
@ -945,7 +951,6 @@
|
|||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
@ -964,6 +969,26 @@
|
|||||||
"integrity": "sha512-gM7TdwuG3amns/1rlgxMbeeyNoBFPa+4Uu0c7FeROWh4qWmvSOnvcslKmWy51ggLKZ2n/F/4i2HJ+PVNxH9uCQ==",
|
"integrity": "sha512-gM7TdwuG3amns/1rlgxMbeeyNoBFPa+4Uu0c7FeROWh4qWmvSOnvcslKmWy51ggLKZ2n/F/4i2HJ+PVNxH9uCQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/engine.io-client": {
|
||||||
|
"version": "6.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.4.0.tgz",
|
||||||
|
"integrity": "sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1",
|
||||||
|
"engine.io-parser": "~5.0.3",
|
||||||
|
"ws": "~8.11.0",
|
||||||
|
"xmlhttprequest-ssl": "~2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/engine.io-parser": {
|
||||||
|
"version": "5.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.6.tgz",
|
||||||
|
"integrity": "sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/esbuild": {
|
"node_modules/esbuild": {
|
||||||
"version": "0.17.15",
|
"version": "0.17.15",
|
||||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz",
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.15.tgz",
|
||||||
@ -1154,8 +1179,7 @@
|
|||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.6",
|
"version": "3.3.6",
|
||||||
@ -1329,6 +1353,32 @@
|
|||||||
"semver": "bin/semver.js"
|
"semver": "bin/semver.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/socket.io-client": {
|
||||||
|
"version": "4.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.6.1.tgz",
|
||||||
|
"integrity": "sha512-5UswCV6hpaRsNg5kkEHVcbBIXEYoVbMQaHJBXJCyEQ+CiFPV1NIOY0XOFWG4XR4GZcB8Kn6AsRs/9cy9TbqVMQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.2",
|
||||||
|
"engine.io-client": "~6.4.0",
|
||||||
|
"socket.io-parser": "~4.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/socket.io-parser": {
|
||||||
|
"version": "4.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.2.tgz",
|
||||||
|
"integrity": "sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
|
"debug": "~4.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/source-map-js": {
|
"node_modules/source-map-js": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||||
@ -1459,6 +1509,34 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||||
|
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": "^5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||||
|
@ -12,7 +12,8 @@
|
|||||||
"chota": "^0.9.2",
|
"chota": "^0.9.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.10.0"
|
"react-router-dom": "^6.10.0",
|
||||||
|
"socket.io-client": "^4.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.0.28",
|
"@types/react": "^18.0.28",
|
||||||
|
3
client/src/components/timerCard.tsx
Normal file
3
client/src/components/timerCard.tsx
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default function TimerCard({ timer }) {
|
||||||
|
return <h1>{timer.name}</h1>;
|
||||||
|
}
|
24
client/src/components/timerHeader.tsx
Normal file
24
client/src/components/timerHeader.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useAuthContext } from "../context/authContext";
|
||||||
|
|
||||||
|
export default function TimerHeader({ onSelect }) {
|
||||||
|
const [friends, setFriends] = useState([]);
|
||||||
|
const { friendName } = useAuthContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/auth/friends")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((friends) => setFriends(friends));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>{friendName}</div>
|
||||||
|
{friends.map((friend) => (
|
||||||
|
<div key={friend.id}>
|
||||||
|
<p>{friend.name}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
39
client/src/hooks/useInitialData.ts
Normal file
39
client/src/hooks/useInitialData.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import { io } from "socket.io-client";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export interface UseInitialDataProps {
|
||||||
|
namespace: string;
|
||||||
|
initialDataEndpoint: string;
|
||||||
|
query?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useInitialData = <T>(props: UseInitialDataProps) => {
|
||||||
|
const [data, setData] = useState<T>();
|
||||||
|
const [query, setQuery] = useState(props?.query);
|
||||||
|
const [socket, setSocket] = useState();
|
||||||
|
|
||||||
|
const refreshData = () =>
|
||||||
|
fetch(props.initialDataEndpoint)
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((r: T) => {
|
||||||
|
setData(r);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshData();
|
||||||
|
|
||||||
|
const socket = io(props.namespace, {
|
||||||
|
query,
|
||||||
|
transports: ["websocket"],
|
||||||
|
});
|
||||||
|
|
||||||
|
setSocket(socket);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.close();
|
||||||
|
setSocket(undefined);
|
||||||
|
};
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return { data, refreshData, query, setQuery, socket, setData };
|
||||||
|
};
|
@ -5,9 +5,9 @@ import "chota";
|
|||||||
|
|
||||||
import { AuthProvider } from "./context/authContext";
|
import { AuthProvider } from "./context/authContext";
|
||||||
|
|
||||||
import Root from "./routes/root";
|
|
||||||
import NotFound from "./routes/notFound";
|
import NotFound from "./routes/notFound";
|
||||||
import Login from "./routes/login";
|
import Login from "./routes/login";
|
||||||
|
import Timers from "./routes/timers";
|
||||||
import ProtectedRoute from "./routes/protected.tsx";
|
import ProtectedRoute from "./routes/protected.tsx";
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
@ -15,7 +15,7 @@ const router = createBrowserRouter([
|
|||||||
path: "/",
|
path: "/",
|
||||||
element: (
|
element: (
|
||||||
<ProtectedRoute>
|
<ProtectedRoute>
|
||||||
<Root />
|
<Timers />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
),
|
),
|
||||||
errorElement: <NotFound />,
|
errorElement: <NotFound />,
|
||||||
@ -27,9 +27,7 @@ const router = createBrowserRouter([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
|
||||||
<React.StrictMode>
|
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</React.StrictMode>
|
|
||||||
);
|
);
|
||||||
|
@ -4,7 +4,7 @@ import { useAuthContext } from "../context/authContext";
|
|||||||
export default function ProtectedRoute({ children }) {
|
export default function ProtectedRoute({ children }) {
|
||||||
const { signedIn } = useAuthContext();
|
const { signedIn } = useAuthContext();
|
||||||
|
|
||||||
if (!signedIn) return <Navigate to="/login" />;
|
// if (!signedIn) return <Navigate to="/login" />;
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { Outlet } from "react-router-dom";
|
|
||||||
|
|
||||||
export default function Root() {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<h1>Hello</h1>
|
|
||||||
<Outlet />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
91
client/src/routes/timers.tsx
Normal file
91
client/src/routes/timers.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import TimerCard from "../components/timerCard";
|
||||||
|
import TimerHeader from "../components/timerHeader";
|
||||||
|
import { useInitialData } from "../hooks/useInitialData";
|
||||||
|
|
||||||
|
export type TimersFilter = {
|
||||||
|
friendId: undefined | number; // when undefined, get all
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimersProps = {
|
||||||
|
filter: TimersFilter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Friend = {
|
||||||
|
id: number;
|
||||||
|
name: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TimerResponse = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
start: Date;
|
||||||
|
created_by: Friend;
|
||||||
|
referenced_friends: Friend[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const putSince = (timers) =>
|
||||||
|
timers.map((timer) => ({
|
||||||
|
...timer,
|
||||||
|
since: "10 seconds ago",
|
||||||
|
}));
|
||||||
|
|
||||||
|
const makeInitialDataEndpoint = (filter: TimersFilter) => {
|
||||||
|
let url = "/api/timers";
|
||||||
|
if (filter && typeof filter.friendId !== "undefined")
|
||||||
|
url += `?id=${filter.friendId}`;
|
||||||
|
return url;
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeQuery = (filter: TimersFilter) => {
|
||||||
|
if (filter && typeof filter.friendId !== "undefined")
|
||||||
|
return { friend: filter.friendId };
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Timers({ filter }: TimersProps) {
|
||||||
|
const {
|
||||||
|
data: timers,
|
||||||
|
refreshData: refreshTimers,
|
||||||
|
setData: setTimers,
|
||||||
|
query,
|
||||||
|
setQuery,
|
||||||
|
socket,
|
||||||
|
} = useInitialData<TimerResponse[]>({
|
||||||
|
initialDataEndpoint: makeInitialDataEndpoint(filter),
|
||||||
|
namespace: "/events/timers",
|
||||||
|
query: makeQuery(filter),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
socket?.on("refreshed", (newTimer: TimerResponse) => {
|
||||||
|
setTimers((timers) =>
|
||||||
|
timers.map((timer) => {
|
||||||
|
if (timer.id === newTimer.id) return newTimer;
|
||||||
|
return timer;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
socket?.on("created", (newTimer: TimerResponse) => {
|
||||||
|
setTimers((timers) => [...timers, newTimer]);
|
||||||
|
});
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTimersInterval = setInterval(() => {
|
||||||
|
setTimers((timers) => putSince(timers));
|
||||||
|
}, 1_000);
|
||||||
|
|
||||||
|
return () => clearInterval(updateTimersInterval);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TimerHeader />
|
||||||
|
{timers?.map((timer) => (
|
||||||
|
<TimerCard timer={timer} key={timer.id} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -18,6 +18,9 @@ services:
|
|||||||
- 8000:80
|
- 8000:80
|
||||||
networks:
|
networks:
|
||||||
- app
|
- app
|
||||||
|
depends_on:
|
||||||
|
- server
|
||||||
|
- client
|
||||||
build:
|
build:
|
||||||
context: ./nginx
|
context: ./nginx
|
||||||
dockerfile: Dockerfile.dev
|
dockerfile: Dockerfile.dev
|
||||||
|
@ -1,23 +1,37 @@
|
|||||||
server {
|
server {
|
||||||
listen 80;
|
listen 80;
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://friendsclient:3000;
|
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /api {
|
location /api {
|
||||||
proxy_pass http://friendsserver:4000;
|
proxy_pass http://friendsserver:4000;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection "upgrade";
|
proxy_set_header Connection "upgrade";
|
||||||
|
}
|
||||||
|
|
||||||
|
location /socket.io {
|
||||||
|
proxy_pass http://friendsserver:4000;
|
||||||
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://friendsclient:3000;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,16 +31,22 @@ export class AuthController {
|
|||||||
return await this.authService.deleteToken(req.token);
|
return await this.authService.deleteToken(req.token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(AuthGuard)
|
||||||
|
@Get('/friends')
|
||||||
|
public async allFriends() {
|
||||||
|
return await this.authService.allFriends();
|
||||||
|
}
|
||||||
|
|
||||||
@Get('/')
|
@Get('/')
|
||||||
async makeGodToken(@Query() query: RetrieveFriendDTO) {
|
public async makeGodToken(@Query() { name, id }: RetrieveFriendDTO) {
|
||||||
const friend = await this.authService.findFriendByName(query.name);
|
const friend = await this.authService.findFriendByNameOrId(name, id);
|
||||||
if (!friend) throw new NotFoundException('Friend not found with that name');
|
if (!friend) throw new NotFoundException('Friend not found by that query');
|
||||||
|
|
||||||
return await this.authService.createTokenForFriend(friend);
|
return await this.authService.createTokenForFriend(friend);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
async verifyFriend(
|
public async verifyFriend(
|
||||||
@Res({ passthrough: true }) res,
|
@Res({ passthrough: true }) res,
|
||||||
@Body() { signature }: SignedGodTokenDTO,
|
@Body() { signature }: SignedGodTokenDTO,
|
||||||
) {
|
) {
|
||||||
@ -59,6 +65,10 @@ export class AuthController {
|
|||||||
);
|
);
|
||||||
if (!referencedToken)
|
if (!referencedToken)
|
||||||
throw new NotFoundException('Could not find God Token to sign');
|
throw new NotFoundException('Could not find God Token to sign');
|
||||||
|
if (referencedToken.signed)
|
||||||
|
throw new BadRequestException(
|
||||||
|
'God Token was already signed - no replay attacks plz',
|
||||||
|
);
|
||||||
|
|
||||||
const { friend } = referencedToken;
|
const { friend } = referencedToken;
|
||||||
const publicKeyObj = await readKey({ armoredKey: friend.public_key });
|
const publicKeyObj = await readKey({ armoredKey: friend.public_key });
|
||||||
|
@ -13,7 +13,8 @@ export class AuthGuard implements CanActivate {
|
|||||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||||
const req = context.switchToHttp().getRequest();
|
const req = context.switchToHttp().getRequest();
|
||||||
|
|
||||||
if (!req.cookies.god_token) throw new UnauthorizedException('No session');
|
if (req && req.cookies && !req.cookies.god_token)
|
||||||
|
throw new UnauthorizedException('No session');
|
||||||
|
|
||||||
const token = await this.authService.findGodTokenWithFriend(
|
const token = await this.authService.findGodTokenWithFriend(
|
||||||
req.cookies.god_token,
|
req.cookies.god_token,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { Friend } from '@prisma/client';
|
import { Friend, Prisma } from '@prisma/client';
|
||||||
import { randomInt } from 'crypto';
|
import { randomInt } from 'crypto';
|
||||||
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
@ -18,12 +18,31 @@ export class AuthService {
|
|||||||
.map(() => words[randomInt(0, words.length)])
|
.map(() => words[randomInt(0, words.length)])
|
||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
|
static FRIEND_SELECT = {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
} as Prisma.FriendSelect;
|
||||||
|
|
||||||
|
public allFriends() {
|
||||||
|
return this.prismaService.friend.findMany({
|
||||||
|
select: AuthService.FRIEND_SELECT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public findFriendByName(name: string) {
|
public findFriendByName(name: string) {
|
||||||
return this.prismaService.friend.findUnique({
|
return this.prismaService.friend.findUnique({
|
||||||
where: { name },
|
where: { name },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findFriendByNameOrId(name: string, id: number) {
|
||||||
|
let where: Prisma.FriendWhereUniqueInput = { name };
|
||||||
|
if (typeof id !== 'undefined') where = { id };
|
||||||
|
return this.prismaService.friend.findUnique({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public createTokenForFriend(friend: Friend) {
|
public createTokenForFriend(friend: Friend) {
|
||||||
return this.prismaService.godToken.create({
|
return this.prismaService.godToken.create({
|
||||||
data: {
|
data: {
|
||||||
|
47
server/src/auth/authServer.adapter.ts
Normal file
47
server/src/auth/authServer.adapter.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { INestApplicationContext } from '@nestjs/common';
|
||||||
|
import { IoAdapter } from '@nestjs/platform-socket.io';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
const parseCookie = (str) =>
|
||||||
|
str
|
||||||
|
.split(';')
|
||||||
|
.map((v) => v.split('='))
|
||||||
|
.reduce((acc, v) => {
|
||||||
|
acc[decodeURIComponent(v[0].trim())] = decodeURIComponent(v[1].trim());
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
/* Shamelessly stolen from https://github.com/nestjs/nest/issues/882#issuecomment-632698668 */
|
||||||
|
export class AuthenticatedSocketIoAdapter extends IoAdapter {
|
||||||
|
private readonly authService: AuthService;
|
||||||
|
constructor(private app: INestApplicationContext) {
|
||||||
|
super(app);
|
||||||
|
this.authService = this.app.get(AuthService);
|
||||||
|
}
|
||||||
|
|
||||||
|
createIOServer(port: number, options?: any): any {
|
||||||
|
options.allowRequest = async (request, allowFunction) => {
|
||||||
|
let verified = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { god_token } = parseCookie(request.headers.cookie);
|
||||||
|
|
||||||
|
const godToken = await this.authService.findGodTokenWithFriend(
|
||||||
|
god_token,
|
||||||
|
);
|
||||||
|
if (godToken && godToken.expiration.getTime() > new Date().getTime())
|
||||||
|
verified = true;
|
||||||
|
} catch (e) {
|
||||||
|
verified = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verified) {
|
||||||
|
return allowFunction(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return allowFunction('Unauthorized', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return super.createIOServer(port, options);
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { IsNotEmpty } from 'class-validator';
|
import { Type } from 'class-transformer';
|
||||||
|
import { IsNotEmpty, ValidateIf } from 'class-validator';
|
||||||
|
|
||||||
export class SignedGodTokenDTO {
|
export class SignedGodTokenDTO {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -6,11 +7,19 @@ export class SignedGodTokenDTO {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RetrieveFriendDTO {
|
export class RetrieveFriendDTO {
|
||||||
@IsNotEmpty()
|
@ValidateIf((rfd) => !rfd.name || rfd.id)
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@ValidateIf((rfd) => !rfd.id || rfd.name)
|
||||||
|
id: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CreateTimerDTO {
|
export class CreateTimerDTO {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class RefreshTimerDTO {
|
||||||
|
@Type(() => Number)
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import { Logger, ValidationPipe } from '@nestjs/common';
|
import { Logger, ValidationPipe } from '@nestjs/common';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { AuthenticatedSocketIoAdapter } from './auth/authServer.adapter';
|
||||||
|
|
||||||
import * as cookieParser from 'cookie-parser';
|
import * as cookieParser from 'cookie-parser';
|
||||||
|
|
||||||
@ -10,6 +11,11 @@ async function bootstrap() {
|
|||||||
|
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.use(cookieParser());
|
app.use(cookieParser());
|
||||||
|
// TODO: Remove
|
||||||
|
app.enableCors();
|
||||||
|
|
||||||
|
// All WS connections must be auth'd
|
||||||
|
app.useWebSocketAdapter(new AuthenticatedSocketIoAdapter(app));
|
||||||
|
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
new ValidationPipe({
|
new ValidationPipe({
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
Get,
|
Get,
|
||||||
Post,
|
Post,
|
||||||
Body,
|
Body,
|
||||||
|
Param,
|
||||||
Req,
|
Req,
|
||||||
Query,
|
Query,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
@ -10,8 +11,13 @@ import {
|
|||||||
BadRequestException,
|
BadRequestException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { TimerService } from './timer.service';
|
import { TimerService } from './timer.service';
|
||||||
|
import { TimerGateway } from './timer.gateway';
|
||||||
import { AuthService } from '../auth/auth.service';
|
import { AuthService } from '../auth/auth.service';
|
||||||
import { CreateTimerDTO, RetrieveFriendDTO } from '../dto/dtos';
|
import {
|
||||||
|
RefreshTimerDTO,
|
||||||
|
CreateTimerDTO,
|
||||||
|
RetrieveFriendDTO,
|
||||||
|
} from '../dto/dtos';
|
||||||
import { AuthGuard } from 'src/auth/auth.guard';
|
import { AuthGuard } from 'src/auth/auth.guard';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
|
||||||
@ -19,20 +25,36 @@ import { Prisma } from '@prisma/client';
|
|||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard)
|
||||||
export class TimerController {
|
export class TimerController {
|
||||||
constructor(
|
constructor(
|
||||||
|
private readonly timerGateway: TimerGateway,
|
||||||
private readonly timerService: TimerService,
|
private readonly timerService: TimerService,
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
public async getAllTimers() {
|
public async getAllTimers() {
|
||||||
return this.timerService.getAll();
|
return await this.timerService.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('/friend')
|
@Get('/friend')
|
||||||
public async getFriendTimers(@Query() { name }: RetrieveFriendDTO) {
|
public async getFriendTimers(@Query() { id, name }: RetrieveFriendDTO) {
|
||||||
const friend = await this.authService.findFriendByName(name);
|
const friend = await this.authService.findFriendByNameOrId(name, id);
|
||||||
if (!friend) throw new NotFoundException('Friend not found with that name');
|
if (!friend) throw new NotFoundException('Friend not found by that query');
|
||||||
return this.timerService.friendTimers(friend);
|
|
||||||
|
return await this.timerService.friendTimers(friend);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('/:id/refresh')
|
||||||
|
public async refreshTimer(@Param() { id }: RefreshTimerDTO, @Req() req) {
|
||||||
|
const timer = await this.timerService.findTimerById(id);
|
||||||
|
if (!timer) throw new NotFoundException('No such timer with id');
|
||||||
|
|
||||||
|
const refreshedTimer = await this.timerService.refreshTimer(
|
||||||
|
timer,
|
||||||
|
req.friend,
|
||||||
|
);
|
||||||
|
this.timerGateway.timerRefreshed(refreshedTimer);
|
||||||
|
|
||||||
|
return refreshedTimer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@ -50,8 +72,9 @@ export class TimerController {
|
|||||||
'Can link no more than 10 unique friends to timer',
|
'Can link no more than 10 unique friends to timer',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let timer;
|
||||||
try {
|
try {
|
||||||
return await this.timerService.createTimerWithFriends(
|
timer = await this.timerService.createTimerWithFriends(
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
created_by: {
|
created_by: {
|
||||||
@ -71,5 +94,9 @@ export class TimerController {
|
|||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.timerGateway.timerCreated(timer);
|
||||||
|
|
||||||
|
return timer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,109 @@
|
|||||||
import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
|
import {
|
||||||
|
WebSocketGateway,
|
||||||
|
WebSocketServer,
|
||||||
|
OnGatewayConnection,
|
||||||
|
OnGatewayDisconnect,
|
||||||
|
} from '@nestjs/websockets';
|
||||||
|
import { Inject, forwardRef } from '@nestjs/common';
|
||||||
|
import { Server, Socket } from 'socket.io';
|
||||||
|
import { TimerService } from './timer.service';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
import { Timer, Friend } from '@prisma/client';
|
||||||
|
|
||||||
@WebSocketGateway()
|
@WebSocketGateway({
|
||||||
export class TimerGateway {
|
transports: ['websocket'],
|
||||||
@SubscribeMessage('message')
|
namespace: '/events/timers',
|
||||||
handleMessage(client: any, payload: any): string {
|
})
|
||||||
return 'Hello world!';
|
export class TimerGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||||
|
constructor(
|
||||||
|
@Inject(forwardRef(() => TimerService))
|
||||||
|
private timerService: TimerService,
|
||||||
|
private authService: AuthService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@WebSocketServer()
|
||||||
|
server: Server;
|
||||||
|
|
||||||
|
private roomsClientsConnected: Map<string, Set<string>> = new Map<
|
||||||
|
string,
|
||||||
|
Set<string>
|
||||||
|
>();
|
||||||
|
|
||||||
|
private clientRoomsConnected: Map<string, Set<string>> = new Map<
|
||||||
|
string,
|
||||||
|
Set<string>
|
||||||
|
>();
|
||||||
|
|
||||||
|
private addClientToRoom(roomName: string, clientId: string) {
|
||||||
|
if (!this.roomsClientsConnected.has(roomName)) {
|
||||||
|
this.roomsClientsConnected.set(roomName, new Set<string>([clientId]));
|
||||||
|
} else {
|
||||||
|
this.roomsClientsConnected.get(roomName).add(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.clientRoomsConnected.has(clientId)) {
|
||||||
|
this.clientRoomsConnected.set(clientId, new Set([roomName]));
|
||||||
|
} else {
|
||||||
|
this.clientRoomsConnected.get(clientId).add(roomName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeClientFromRoom(roomName: string, clientId: string) {
|
||||||
|
if (this.roomsClientsConnected.has(roomName)) {
|
||||||
|
const clients = this.roomsClientsConnected.get(roomName);
|
||||||
|
|
||||||
|
if (clients.size === 1) {
|
||||||
|
this.roomsClientsConnected.delete(roomName);
|
||||||
|
} else {
|
||||||
|
clients.delete(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.clientRoomsConnected.has(clientId)) {
|
||||||
|
const clientRooms = this.clientRoomsConnected.get(clientId);
|
||||||
|
clientRooms.delete(roomName);
|
||||||
|
|
||||||
|
if (clientRooms.size === 0) this.clientRoomsConnected.delete(clientId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private friendRoom = ({ id }: { id: number }) => `friend-${id}`;
|
||||||
|
|
||||||
|
public async handleConnection(client: Socket) {
|
||||||
|
let roomName = 'all';
|
||||||
|
|
||||||
|
const { friend } = client.handshake?.query;
|
||||||
|
if (friend) {
|
||||||
|
const listenFriend = isNaN(Number(friend))
|
||||||
|
? await this.authService.findFriendByName(friend as string)
|
||||||
|
: await this.authService.findFriendByNameOrId('', Number(friend));
|
||||||
|
roomName = this.friendRoom(listenFriend);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.join(roomName);
|
||||||
|
this.addClientToRoom(roomName, client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleDisconnect(client: Socket) {
|
||||||
|
for (const room of this.clientRoomsConnected.get(client.id))
|
||||||
|
this.removeClientFromRoom(room, client.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public timerCreated(timer: Timer & { referenced_friends: Friend[] }) {
|
||||||
|
this.server.to('all').emit('created', timer);
|
||||||
|
timer.referenced_friends.map((friend) =>
|
||||||
|
this.server.to(this.friendRoom(friend)).emit('created', timer),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public timerRefreshed(
|
||||||
|
timer: Partial<Timer> & { referenced_friends: Partial<Friend>[] },
|
||||||
|
) {
|
||||||
|
this.server.to('all').emit('refreshed', timer);
|
||||||
|
timer.referenced_friends.map((friend) =>
|
||||||
|
this.server
|
||||||
|
.to(this.friendRoom(friend as Friend))
|
||||||
|
.emit('refreshed', timer),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,30 +1,32 @@
|
|||||||
import { Friend, Prisma } from '@prisma/client';
|
import { Friend, Timer, Prisma } from '@prisma/client';
|
||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
import { PrismaService } from '../prisma/prisma.service';
|
import { PrismaService } from '../prisma/prisma.service';
|
||||||
|
import { AuthService } from '../auth/auth.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class TimerService {
|
export class TimerService {
|
||||||
constructor(private readonly prismaService: PrismaService) {}
|
constructor(private readonly prismaService: PrismaService) {}
|
||||||
|
|
||||||
public getAll() {
|
static TIMER_SELECT = {
|
||||||
return this.prismaService.timer.findMany({
|
|
||||||
select: {
|
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
start: true,
|
start: true,
|
||||||
|
} as Prisma.TimerSelect;
|
||||||
|
|
||||||
|
static INCLUDE_FRIENDS_SELECT = {
|
||||||
referenced_friends: {
|
referenced_friends: {
|
||||||
select: {
|
select: AuthService.FRIEND_SELECT,
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created_by: {
|
created_by: {
|
||||||
|
select: AuthService.FRIEND_SELECT,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
public getAll() {
|
||||||
|
return this.prismaService.timer.findMany({
|
||||||
select: {
|
select: {
|
||||||
name: true,
|
...TimerService.TIMER_SELECT,
|
||||||
id: true,
|
...TimerService.INCLUDE_FRIENDS_SELECT,
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -32,41 +34,82 @@ export class TimerService {
|
|||||||
public friendTimers(friend: Friend) {
|
public friendTimers(friend: Friend) {
|
||||||
return this.prismaService.timer.findMany({
|
return this.prismaService.timer.findMany({
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
...TimerService.TIMER_SELECT,
|
||||||
name: true,
|
|
||||||
start: true,
|
|
||||||
referenced_friends: {
|
referenced_friends: {
|
||||||
where: {
|
where: {
|
||||||
id: friend.id,
|
id: friend.id,
|
||||||
},
|
},
|
||||||
select: {
|
select: AuthService.FRIEND_SELECT,
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
created_by: {
|
created_by: {
|
||||||
select: {
|
select: AuthService.FRIEND_SELECT,
|
||||||
name: true,
|
|
||||||
id: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public findTimerById(id: number) {
|
||||||
|
return this.prismaService.timer.findUnique({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshTimer(timer: Timer, friend: Friend) {
|
||||||
|
const now = new Date();
|
||||||
|
const select = {
|
||||||
|
...TimerService.TIMER_SELECT,
|
||||||
|
...TimerService.INCLUDE_FRIENDS_SELECT,
|
||||||
|
};
|
||||||
|
const refreshedTimer = await this.prismaService.timer.update({
|
||||||
|
where: { id: timer.id },
|
||||||
|
data: {
|
||||||
|
start: now,
|
||||||
|
},
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.prismaService.timerRefreshes.create({
|
||||||
|
data: {
|
||||||
|
start: timer.start,
|
||||||
|
end: now,
|
||||||
|
refreshed_by: {
|
||||||
|
connect: {
|
||||||
|
id: friend.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timer: {
|
||||||
|
connect: {
|
||||||
|
id: timer.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return refreshedTimer;
|
||||||
|
}
|
||||||
|
|
||||||
public createTimerWithFriends(
|
public createTimerWithFriends(
|
||||||
timer: Prisma.TimerCreateInput,
|
timer: Prisma.TimerCreateInput,
|
||||||
friendIds: number[],
|
referencedFriendIds: number[],
|
||||||
) {
|
) {
|
||||||
if (friendIds.length > 0)
|
const select = {
|
||||||
|
...TimerService.TIMER_SELECT,
|
||||||
|
...TimerService.INCLUDE_FRIENDS_SELECT,
|
||||||
|
};
|
||||||
|
if (referencedFriendIds.length > 0)
|
||||||
return this.prismaService.timer.create({
|
return this.prismaService.timer.create({
|
||||||
data: {
|
data: {
|
||||||
...timer,
|
...timer,
|
||||||
referenced_friends: {
|
referenced_friends: {
|
||||||
connect: friendIds.map((id) => ({ id })),
|
connect: referencedFriendIds.map((id) => ({ id })),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
select,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.prismaService.timer.create({
|
||||||
|
data: timer,
|
||||||
|
select,
|
||||||
});
|
});
|
||||||
return this.prismaService.timer.create({ data: timer });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user