Starting to implement full-stack logic

This commit is contained in:
aronmal 2023-04-12 22:32:40 +02:00
parent 06f0eacc78
commit a32b40395e
Signed by: aronmal
GPG key ID: 816B7707426FC612
13 changed files with 211 additions and 57 deletions

View file

@ -1,24 +1,20 @@
// import Bluetooth from './Bluetooth'
import BorderTiles from "./BorderTiles"
import EventBar from "./EventBar"
// import FogImages from './FogImages'
import HitElems from "./HitElems"
import Labeling from "./Labeling"
import Ships from "./Ships"
import Targets from "./Targets"
import useGameEvent from "@lib/hooks/useGameEvent"
import { CSSProperties } from "react"
function Gamefield() {
const count = 12
const { pointersProps, targetsProps, tilesProps, hits } = useGameEvent(count)
const { BorderTiles, HitElems, Targets, EventBar } = useGameEvent(count)
return (
<div id="gamefield">
{/* <Bluetooth /> */}
<div id="game-frame" style={{ "--i": count } as CSSProperties}>
{/* Bordes */}
<BorderTiles props={tilesProps} />
<BorderTiles />
{/* Collumn lettes and row numbers */}
<Labeling count={count} />
@ -26,14 +22,14 @@ function Gamefield() {
{/* Ships */}
<Ships />
<HitElems hits={hits} />
<HitElems />
{/* Fog images */}
{/* <FogImages /> */}
<Targets props={pointersProps} />
<Targets />
{/* <span id='dev-debug' style={{gridArea: '1 / 12 / 1 / 15', backgroundColor: 'red', zIndex: 3} as CSSProperties}>Debug</span> */}
</div>
<EventBar props={targetsProps} />
<EventBar />
</div>
)
}

View file

@ -1,8 +1,10 @@
import { gameContext } from "../../pages/_app"
import Icon from "./Icon"
import Player from "./Player"
import { Fragment, useEffect, useState } from "react"
import { Fragment, useContext, useEffect, useState } from "react"
function LobbyFrame({ openSettings }: { openSettings: () => void }) {
const [gameProps, setGameProps] = useContext(gameContext)
const [enemy, setEnemy] = useState(false)
const [dots, setDots] = useState(1)
@ -17,7 +19,7 @@ function LobbyFrame({ openSettings }: { openSettings: () => void }) {
<div className="flex items-center justify-between border-b-2 border-slate-900">
<Icon src="speech_bubble.png">Chat</Icon>
<h1 className="font-farro text-5xl font-medium">
Game-PIN: <span className="underline">3169</span>
Game-PIN: <span className="underline">{gameProps.pin}</span>
</h1>
<Icon src="gear.png" onClick={openSettings}>
Settings

View file

@ -0,0 +1,35 @@
import { rejectionErrorFns } from "../errors"
import jwtVerifyCatch from "../jwtVerifyCatch"
import logging from "../logging"
import type { IdToken, RawToken } from "./createTokenDB"
import sendError, { API } from "./sendError"
import jwt from "jsonwebtoken"
async function decodeToken<T>(
context: API<T>,
rawToken: RawToken,
next: (token: IdToken) => void
) {
const { value, type } = rawToken
// Verify the token and get the payload
let data: string | jwt.JwtPayload
try {
data = jwt.verify(value, process.env.TOKEN_SECRET as string)
} catch (err: any) {
// Deal with the problem in more detail
logging(jwtVerifyCatch(type, err).message, ["error"])
const fallbackData = jwt.decode(value)
// Making sure the token data is not a string (because it should be an object)
if (typeof fallbackData === "string")
return sendError(context, rejectionErrorFns.tokenWasString(type, value))
return next({ id: fallbackData?.id, type })
}
// Making sure the token data is not a string (because it should be an object)
if (typeof data === "string")
return sendError(context, rejectionErrorFns.tokenWasString(type, value))
return next({ id: data.id, type })
}
export default decodeToken

View file

@ -7,7 +7,8 @@ import type { Token } from "@prisma/client"
async function getTokenDB<T>(
context: API<T>,
token: IdToken,
next: (tokenDB: Token) => void
next: (tokenDB: Token) => void,
ignoreChecks?: boolean
) {
const { id, type } = token
// Find refresh token in DB
@ -18,14 +19,15 @@ async function getTokenDB<T>(
})
if (!tokenDB) return sendError(context, rejectionErrorFns.tokenNotFound(type))
if (tokenDB.used) return sendError(context, rejectionErrors.tokenUsed)
if (tokenDB.used && !ignoreChecks)
return sendError(context, rejectionErrors.tokenUsed)
await prisma.token.update({
where: {
id,
},
data: {
used: true,
used: type === "ACCESS",
},
})

View file

@ -1,18 +1,32 @@
import createPlayerDB from "./createPlayerDB"
import createTokenDB, { RawToken } from "./createTokenDB"
import type { API } from "./sendError"
import { setCookie } from "cookies-next"
async function getTokenFromCookie<T>(
context: API<T>,
next: (refreshToken: RawToken) => void
) {
const type = "REFRESH"
const value = context.req.cookies.token
const { req, res } = context
const value = req.cookies.token
// Checking for cookie presens, because it is necessary
if (!value) {
return createPlayerDB((player) =>
createTokenDB(player, type, (newToken) => next(newToken))
createTokenDB(player, type, (newToken) => {
// Set login cookie
setCookie("token", newToken.value, {
req,
res,
maxAge: 172800,
httpOnly: true,
sameSite: true,
secure: true,
path: "/api",
})
return next(newToken)
})
)
}
return next({ value, type })

View file

@ -1,4 +1,8 @@
import BorderTiles from "../../components/Gamefield/BorderTiles"
import EventBar from "../../components/Gamefield/EventBar"
import type { PointerProps } from "../../components/Gamefield/GamefieldPointer"
import HitElems from "../../components/Gamefield/HitElems"
import Targets from "../../components/Gamefield/Targets"
import {
Hit,
Mode,
@ -6,6 +10,7 @@ import {
Target,
Position,
} from "../../interfaces/frontend"
import { gameContext } from "../../pages/_app"
import {
hitReducer,
initlialLastLeftTile,
@ -13,7 +18,7 @@ import {
initlialTargetPreview,
initlialMouseCursor,
} from "../utils/helpers"
import { useCallback, useEffect, useReducer, useState } from "react"
import { useCallback, useContext, useEffect, useReducer, useState } from "react"
const modes: Mode[] = [
{
@ -28,10 +33,14 @@ const modes: Mode[] = [
pointerGrid: Array.from(Array(1), () => Array.from(Array(3))),
type: "vtorpedo",
},
{ pointerGrid: [[{ x: 0, y: 0 }]], type: "missile" },
{
pointerGrid: Array.from(Array(1), () => Array.from(Array(1))),
type: "missile",
},
]
function useGameEvent(count: number) {
const [gameProps, setGameProps] = useContext(gameContext)
const [lastLeftTile, setLastLeftTile] =
useState<Position>(initlialLastLeftTile)
const [target, setTarget] = useState<Target>(initlialTarget)
@ -208,10 +217,16 @@ function useGameEvent(count: number) {
}, [targetPreview.show])
return {
tilesProps: { count, settingTarget, setMouseCursor, setLastLeftTile },
pointersProps: { composeTargetTiles, target, targetPreview },
targetsProps: { setMode, setTarget },
hits,
BorderTiles: () => (
<BorderTiles
props={{ count, settingTarget, setMouseCursor, setLastLeftTile }}
/>
),
HitElems: () => <HitElems hits={hits} />,
Targets: () => (
<Targets props={{ composeTargetTiles, target, targetPreview }} />
),
EventBar: () => <EventBar props={{ setMode, setTarget }} />,
}
}

View file

@ -32,7 +32,8 @@
"react-dom": "18.2.0",
"socket.io": "^4.6.1",
"socket.io-client": "^4.6.1",
"typescript": "4.9.4"
"typescript": "4.9.4",
"zod": "^3.21.4"
},
"devDependencies": {
"@total-typescript/ts-reset": "^0.3.7",

View file

@ -3,7 +3,28 @@ import "../styles/globals.css"
import "../styles/grid2.scss"
import "../styles/grid.scss"
import type { AppProps } from "next/app"
import { Dispatch, SetStateAction, createContext, useState } from "react"
interface gameContext {
pin?: string
game?: {
id: ""
}
enemy?: {
id: string
username?: string
}
}
export const gameContext = createContext<
[gameContext, Dispatch<SetStateAction<gameContext>>]
>([{}, () => {}])
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />
const gameProps = useState<gameContext>({})
return (
<gameContext.Provider value={gameProps}>
<Component {...pageProps} />
</gameContext.Provider>
)
}

View file

@ -2,12 +2,18 @@ import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import getPlayer from "@lib/backend/components/getPlayer"
import prisma from "@lib/prisma"
import type { Game } from "@prisma/client"
import type { NextApiRequest, NextApiResponse } from "next"
import { z } from "zod"
interface Data {
game: Game
}
const returnSchema = z.object({
game: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
running: z.boolean(),
}),
})
type Data = z.infer<typeof returnSchema>
export default async function create(
req: NextApiRequest,

View file

@ -18,19 +18,19 @@ export default async function auth(
const context = { req, res }
const type = "ACCESS"
getTokenFromCookie(context, (refreshToken) => {
checkTokenIsValid(context, refreshToken, (token) => {
getTokenDB(context, token, (tokenDB) => {
getPlayerByIdDB(context, tokenDB, (player) => {
createTokenDB(player, type, (newToken, newTokenDB) => {
return getTokenFromCookie(context, (refreshToken) =>
checkTokenIsValid(context, refreshToken, (token) =>
getTokenDB(context, token, (tokenDB) =>
getPlayerByIdDB(context, tokenDB, (player) =>
createTokenDB(player, type, (newToken, newTokenDB) =>
sendResponse(context, {
message: `Access-Token generated: ${newTokenDB.id} with Refreshtoken-Token: ${tokenDB.id}`,
body: { token: newToken.value },
type: ["debug", "infoCyan"],
})
})
})
})
})
}).catch((err) => sendError(context, err))
)
)
)
)
).catch((err) => sendError(context, err))
}

View file

@ -1,8 +1,8 @@
import checkTokenIsValid from "@backend/components/checkTokenIsValid"
import getTokenDB from "@backend/components/getTokenDB"
import getTokenFromCookie from "@backend/components/getTokenFromCookie"
import sendError from "@backend/components/sendError"
import sendResponse from "@backend/components/sendResponse"
import decodeToken from "@lib/backend/components/decodeToken"
import { deleteCookie } from "cookies-next"
import type { NextApiRequest, NextApiResponse } from "next"
@ -16,18 +16,23 @@ export default async function logout(
) {
const context = { req, res }
return getTokenFromCookie(context, (refreshToken) => {
checkTokenIsValid(context, refreshToken, (token) => {
getTokenDB(context, token, (tokenDB) => {
// Set login cookie
deleteCookie("token", { req, res })
return getTokenFromCookie(context, (refreshToken) =>
decodeToken(context, refreshToken, (token) =>
getTokenDB(
context,
token,
(tokenDB) => {
// Set login cookie
deleteCookie("token", { req, res })
sendResponse(context, {
message: "User of Token " + tokenDB.id + " logged out.",
body: { loggedOut: true },
type: ["debug", "infoCyan"],
})
})
})
}).catch((err) => sendError(context, err))
return sendResponse(context, {
message: "User of Token " + tokenDB.id + " logged out.",
body: { loggedOut: true },
type: ["debug", "infoCyan"],
})
},
true
)
)
).catch((err) => sendError(context, err))
}

View file

@ -6,18 +6,23 @@ import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
import { faCirclePlay } from "@fortawesome/pro-thin-svg-icons"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import getAccessToken from "@lib/frontend/getAccessToken"
import { GetServerSideProps } from "next"
import { useRouter } from "next/router"
import { useState } from "react"
import { z } from "zod"
export default function Home() {
interface Props {
start: boolean
}
export default function Home({ start }: Props) {
const router = useRouter()
const [heWantsToPlay, setHeWantsToPlay] = useState<boolean | null>(false)
return (
<div className="h-full bg-theme">
<div className="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo />
<BurgerMenu />
{!heWantsToPlay ? (
{!start ? (
<>
<div className="flex h-36 w-64 items-center justify-center rounded-xl border-4 border-black bg-[#2227] sm:h-48 sm:w-96 md:h-72 md:w-[32rem] md:border-[6px] xl:h-[26rem] xl:w-[48rem]">
<FontAwesomeIcon
@ -27,7 +32,15 @@ export default function Home() {
</div>
<button
className="font-farro rounded-lg border-b-4 border-orange-400 bg-warn px-12 pb-4 pt-5 text-2xl font-bold duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:border-b-[6px] sm:px-14 sm:pb-5 sm:pt-6 sm:text-3xl sm:active:border-t-[6px] md:rounded-2xl md:border-b-8 md:px-20 md:pb-6 md:pt-7 md:text-4xl md:active:border-t-8 xl:px-24 xl:pb-8 xl:pt-10 xl:text-5xl"
onClick={() => setTimeout(() => setHeWantsToPlay(true), 200)}
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `true`
router.push({
pathname: router.pathname,
query: { start: true },
})
}, 200)
}
>
START
</button>
@ -36,7 +49,14 @@ export default function Home() {
<div className="flex flex-col items-center rounded-xl border-4 border-black bg-grayish px-4 py-6 shadow-lg sm:mx-8 sm:p-12 md:w-full">
<button
className="-mt-2 w-20 self-start rounded-xl border-b-4 border-shield-gray bg-voidDark text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:-mt-6 sm:w-40 sm:px-2 sm:text-5xl"
onClick={() => setTimeout(() => setHeWantsToPlay(false), 200)}
onClick={() =>
setTimeout(() => {
// Navigate to the same page with the `start` query parameter set to `false`
router.push({
pathname: router.pathname,
})
}, 200)
}
>
<FontAwesomeIcon icon={faLeftLong} />
</button>
@ -48,6 +68,25 @@ export default function Home() {
method: "POST",
body: JSON.stringify({ token }),
})
const gameSchema = z.object({
game: z.object({
id: z.string(),
createdAt: z.date(),
updatedAt: z.date(),
running: z.boolean(),
}),
})
const check = gameSchema.safeParse(game)
if (check.success) {
const gameData = check.data
console.log(gameData)
} else {
console.error(check.error)
}
// const warst = result.game
router.push("/dev/lobby")
}}
@ -64,3 +103,14 @@ export default function Home() {
</div>
)
}
export const getServerSideProps: GetServerSideProps<Props> = async (
context
) => {
const { start } = context.query
// Convert the `start` query parameter to a boolean
const isStart = start === "true"
return { props: { start: isStart } }
}

View file

@ -73,6 +73,9 @@ dependencies:
typescript:
specifier: 4.9.4
version: 4.9.4
zod:
specifier: ^3.21.4
version: 3.21.4
devDependencies:
'@total-typescript/ts-reset':
@ -3397,3 +3400,7 @@ packages:
/yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
/zod@3.21.4:
resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==}
dev: false