Migrated to latest solid-start version

This commit is contained in:
aronmal 2024-02-22 17:11:47 +01:00
parent 777b807225
commit 6fb057a102
Signed by: aronmal
GPG key ID: 816B7707426FC612
42 changed files with 4727 additions and 2128 deletions

View file

@ -1,14 +0,0 @@
module.exports = {
parser: "@typescript-eslint/parser",
parserOptions: {
project: ["./tsconfig.eslint.json", "./tsconfig.json"],
tsconfigRootDir: __dirname,
},
plugins: ["@typescript-eslint", "solid"],
extends: [
"plugin:solid/typescript",
"plugin:@typescript-eslint/recommended",
"prettier",
],
ignorePatterns: ["dist/**"],
}

View file

@ -0,0 +1,11 @@
{
"parser": "@typescript-eslint/parser",
"env": {
"node": true
},
"plugins": ["solid"],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:solid/typescript"
]
}

View file

@ -1,44 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# logs
/log
src/drizzle/migrations
# drizzle
/drizzle/migrations
dist
.vinxi
.output
.vercel
.netlify
netlify
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# SolidJS
/.solid/
/dist/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
# Environment
.env
.env*.local
# typescript
*.tsbuildinfo
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
*.launch
.settings/
# Temp
gitignore
# System Files
.DS_Store
Thumbs.db
# playwright
/test-results/
/playwright-report/
/playwright/.cache/
/playwright/.cache/

View file

@ -1,9 +1,9 @@
{
"name": "leaky-ships",
"scripts": {
"dev": "solid-start dev --port 3000",
"start": "solid-start start --port 3000",
"build": "solid-start build",
"dev": "vinxi dev",
"build": "vinxi build",
"start": "vinxi start",
"lint": "eslint --fix \"**/*.{ts,tsx,js,jsx}\"",
"push": "drizzle-kit push:pg",
"test": "pnpm playwright test --ui",
@ -11,64 +11,65 @@
},
"type": "module",
"dependencies": {
"@auth/core": "^0.13.0",
"@auth/drizzle-adapter": "^0.3.2",
"@auth/solid-start": "^0.1.1",
"@fortawesome/fontawesome-svg-core": "^6.4.2",
"@fortawesome/pro-duotone-svg-icons": "^6.4.2",
"@fortawesome/pro-light-svg-icons": "^6.4.2",
"@fortawesome/pro-regular-svg-icons": "^6.4.2",
"@fortawesome/pro-solid-svg-icons": "^6.4.2",
"@fortawesome/pro-thin-svg-icons": "^6.4.2",
"@fortawesome/sharp-solid-svg-icons": "^6.4.2",
"@auth/core": "^0.27.0",
"@auth/drizzle-adapter": "^0.7.0",
"@auth/solid-start": "^0.6.1",
"@fortawesome/fontawesome-svg-core": "^6.5.1",
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
"@fortawesome/pro-light-svg-icons": "^6.5.1",
"@fortawesome/pro-regular-svg-icons": "^6.5.1",
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
"@paralleldrive/cuid2": "^2.2.2",
"@solidjs/meta": "^0.28.6",
"@solidjs/router": "^0.8.3",
"classnames": "^2.3.2",
"@solidjs/meta": "^0.29.3",
"@solidjs/router": "^0.12.4",
"@solidjs/start": "^0.5.9",
"classnames": "^2.5.1",
"colors": "^1.4.0",
"drizzle-orm": "^0.28.6",
"drizzle-orm": "^0.29.4",
"drizzle-zod": "^0.5.1",
"http-status": "^1.7.0",
"json-stable-stringify": "^1.0.2",
"http-status": "^1.7.3",
"json-stable-stringify": "^1.1.1",
"lodash-es": "^4.17.21",
"nodemailer": "^6.9.5",
"nodemailer": "^6.9.10",
"object-hash": "^3.0.0",
"postgres": "^3.3.5",
"socket.io": "^4.7.2",
"socket.io-client": "^4.7.2",
"postgres": "^3.4.3",
"socket.io": "^4.7.4",
"socket.io-client": "^4.7.4",
"solid-color": "^0.0.4",
"solid-js": "^1.7.11",
"solid-start": "^0.3.5",
"solid-js": "^1.8.15",
"tinycolor2": "^1.6.0",
"unique-names-generator": "^4.7.1",
"zod": "3.21.1"
"vinxi": "^0.3.3",
"zod": "3.22.4"
},
"packageManager": "pnpm@8.7.4",
"devDependencies": {
"@playwright/test": "^1.37.1",
"@total-typescript/ts-reset": "^0.4.2",
"@types/json-stable-stringify": "^1.0.34",
"@types/node": "^20.5.9",
"@types/nodemailer": "^6.4.9",
"@types/object-hash": "^3.0.4",
"@types/web-bluetooth": "^0.0.17",
"@typescript-eslint/eslint-plugin": "^6.6.0",
"autoprefixer": "^10.4.15",
"dotenv": "^16.3.1",
"drizzle-kit": "^0.19.13",
"eslint": "^8.48.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-solid": "^0.12.1",
"@playwright/test": "^1.41.2",
"@total-typescript/ts-reset": "^0.5.1",
"@types/json-stable-stringify": "^1.0.36",
"@types/node": "^20.11.19",
"@types/nodemailer": "^6.4.14",
"@types/object-hash": "^3.0.6",
"@types/web-bluetooth": "^0.0.20",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"autoprefixer": "^10.4.17",
"dotenv": "^16.4.5",
"drizzle-kit": "^0.20.14",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-solid": "^0.13.1",
"pg": "^8.11.3",
"postcss": "^8.4.29",
"prettier": "^3.0.3",
"prettier-plugin-organize-imports": "^3.2.3",
"prettier-plugin-tailwindcss": "^0.5.4",
"sass": "^1.66.1",
"solid-start-node": "^0.3.5",
"tailwindcss": "^3.3.3",
"typescript": "^5.2.2",
"vite": "^4.4.9"
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-tailwindcss": "^0.5.11",
"sass": "^1.71.1",
"solid-start-node": "^0.3.10",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.4"
},
"pnpm": {
"overrides": {

File diff suppressed because it is too large Load diff

39
leaky-ships/src/app.tsx Normal file
View file

@ -0,0 +1,39 @@
// @refresh reload
import "@fortawesome/fontawesome-svg-core/styles.css"
import { Link, Meta, MetaProvider, Title } from "@solidjs/meta"
import { Router } from "@solidjs/router"
import { FileRoutes } from "@solidjs/start"
import { Suspense } from "solid-js"
import "./styles/App.scss"
import "./styles/globals.scss"
import "./styles/grid.scss"
import "./styles/grid2.scss"
import "./styles/root.css"
export default function App() {
return (
<Router
root={(props) => (
<MetaProvider>
<Title>Leaky Ships</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Link rel="manifest" href="/manifest.json" />
<Link rel="icon" href="/favicon.ico" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta name="theme-color" content="#000000" />
<Meta
name="description"
content="Battleship web app with react frontend and ASP .NET backend"
/>
<Link rel="apple-touch-icon" href="/logo192.png" />
<Suspense>{props.children}</Suspense>
</MetaProvider>
)}
>
<FileRoutes />
</Router>
)
}

View file

@ -24,8 +24,8 @@ import {
import { socket } from "~/lib/socket"
import { modes } from "~/lib/utils/helpers"
// import { Icons, toast } from "react-toastify"
import { useNavigate } from "@solidjs/router"
import { For, Show, createEffect } from "solid-js"
import { useNavigate } from "solid-start"
import { clearDrawing } from "~/hooks/useDraw"
import {
color,

View file

@ -1,8 +1,8 @@
// import Bluetooth from "./Bluetooth"
// import FogImages from "./FogImages"
// import { toast } from "react-toastify"
import { useNavigate } from "@solidjs/router"
import { createEffect, onCleanup } from "solid-js"
import { useNavigate } from "solid-start"
import BorderTiles from "~/components/Gamefield/BorderTiles"
import EventBar from "~/components/Gamefield/EventBar"
import HitElems from "~/components/Gamefield/HitElems"

View file

@ -2,8 +2,8 @@ import {
faRightFromBracket,
faSpinnerThird,
} from "@fortawesome/pro-solid-svg-icons"
import { useNavigate } from "@solidjs/router"
import { JSX, Show, createEffect, createSignal, onCleanup } from "solid-js"
import { useNavigate } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { full, gameProps, leave, reset, users } from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession"

View file

@ -1,9 +1,8 @@
import classNames from "classnames"
import { A } from "solid-start"
function Logo(props: { small?: boolean }) {
return (
<A href="/">
<a href="/">
<div class="relative flex flex-col items-center rounded-sm border-x-4 border-y-2 border-shield-gray bg-shield-lightgray md:border-x-8 md:border-y-4">
<h1
class={classNames(
@ -16,7 +15,7 @@ function Logo(props: { small?: boolean }) {
</h1>
<Screws small={props.small} />
</div>
</A>
</a>
)
}

View file

@ -1,7 +1,6 @@
import { IconDefinition } from "@fortawesome/fontawesome-svg-core"
import classNames from "classnames"
import { JSX } from "solid-js"
import { A } from "solid-start"
import { FontAwesomeIcon } from "./FontAwesomeIcon"
const styles = {
@ -20,7 +19,7 @@ export function OptionAnchor(props: {
disabled?: boolean
}) {
return (
<A
<a
class={classNames(
styles.wrapper,
props.disabled ? styles.disabled : styles.enabled,
@ -30,7 +29,7 @@ export function OptionAnchor(props: {
>
<span class="mx-auto">{props.text}</span>
<FontAwesomeIcon class={styles.icon} icon={props.icon} />
</A>
</a>
)
}

View file

@ -2,7 +2,7 @@ import { drizzle } from "drizzle-orm/postgres-js"
import postgres from "postgres"
import * as schema from "./schemas/Tables"
const queryClient = postgres(process.env.DATABASE_URL ?? "")
const queryClient = postgres(import.meta.env.VITE_DATABASE_URL ?? "")
const db = drizzle(queryClient, {
schema,
})

View file

@ -1,3 +1,3 @@
import { mount, StartClient } from "solid-start/entry-client"
import { mount, StartClient } from "@solidjs/start/client"
mount(() => <StartClient />, document)
mount(() => <StartClient />, document.getElementById("app")!)

View file

@ -1,9 +1,20 @@
import {
createHandler,
renderAsync,
StartServer,
} from "solid-start/entry-server"
import { StartServer, createHandler } from "@solidjs/start/server"
export default createHandler(
renderAsync((event) => <StartServer event={event} />),
)
export default createHandler(() => (
<StartServer
document={({ assets, children, scripts }) => (
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/favicon.ico" />
{assets}
</head>
<body id="app">
{children}
{scripts}
</body>
</html>
)}
/>
))

View file

@ -1,4 +1,7 @@
import { Session } from "@auth/core/types"
import { getSession } from "@auth/solid-start"
import { useIsRouting } from "@solidjs/router"
import status from "http-status"
import stringify from "json-stable-stringify"
import {
JSX,
@ -8,7 +11,8 @@ import {
createSignal,
useContext,
} from "solid-js"
import { useIsRouting } from "solid-start"
import { getRequestEvent } from "solid-js/web"
import { authOptions } from "~/server/auth"
import { gameProps, setGameProps, users } from "./useGameProps"
const [state, setState] = createSignal<Session | null | undefined>(undefined)
@ -61,12 +65,17 @@ const contextValue = {
}
export const SessionCtx = createContext(contextValue)
export async function getSessionFromServer() {
"use server"
const event = getRequestEvent()
if (!event) return
const session = await getSession(event.request, authOptions)
if (session) return session
else return null
}
export function SessionProvider(props: { children: JSX.Element }) {
const fetchData = () =>
fetch("/api/session").then((res) =>
res.ok ? (res.json() as Promise<Session>) : null,
)
const [data, { refetch }] = createResource(fetchData)
const [data, { refetch }] = createResource(() => getSessionFromServer())
const isRouting = useIsRouting()
createEffect(() => {
@ -77,8 +86,9 @@ export function SessionProvider(props: { children: JSX.Element }) {
createEffect(() => {
const session = data()
const hashDiff = stringify(session) !== stringify(state())
if (session === undefined || data.loading || !hashDiff) return
if (!session || !hashDiff) return
console.log("Session updated.")
// @ts-ignore
setState(session)
})
@ -103,3 +113,14 @@ export function SessionProvider(props: { children: JSX.Element }) {
}
export const useSession = () => useContext(SessionCtx)
export function isAuthenticated(res: Response) {
switch (status[`${res.status}_CLASS`]) {
case status.classes.SUCCESSFUL:
case status.classes.REDIRECTION:
return res.json()
}
const resStatus = status[`${res.status}_CLASS`]
if (typeof resStatus !== "string") return
}

View file

@ -1,12 +1,11 @@
import status from "http-status"
// import { toast } from "react-toastify"
import { useNavigate } from "@solidjs/router"
import { createEffect, createSignal, onCleanup } from "solid-js"
import { useNavigate } from "solid-start"
import { getPayloadFromProps } from "~/lib/getPayloadFromProps"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { socket } from "~/lib/socket"
import { GamePropsSchema, GameState } from "~/lib/zodSchemas"
import { isAuthenticated } from "~/routes/start"
import { GameSettings, PlayerEvent } from "../interfaces/frontend"
import {
DispatchMove,
@ -20,7 +19,7 @@ import {
setShips,
users,
} from "./useGameProps"
import { useSession } from "./useSession"
import { isAuthenticated, useSession } from "./useSession"
/** This function should only be called once per page, otherwise there will be multiple socket connections and duplicate event listeners. */
function useSocket() {

View file

@ -1,5 +1,6 @@
import { Session } from "@auth/core/types"
import http from "http"
import type { Server as HTTPServer } from "http"
import type { Socket as NetSocket } from "net"
import type {
Server as IOServer,
Server,
@ -16,10 +17,14 @@ import {
ShipProps,
} from "./frontend"
export interface SocketServer extends http.Server {
interface SocketServer extends HTTPServer {
io?: IOServer
}
export interface SocketWithIO extends NetSocket {
server: SocketServer
}
export interface ServerToClientEvents {
gameSetting: (payload: GameSettings, hash: string) => void
playerEvent: (event: PlayerEvent) => void

View file

@ -1,4 +1,4 @@
import { APIEvent } from "solid-start"
import { APIEvent } from "@solidjs/start/server/types"
import { z } from "zod"
import sendError from "./sendError"

View file

@ -1,7 +1,7 @@
import { APIEvent } from "@solidjs/start/server/types"
import colors, { Color } from "colors"
import fs from "fs"
import { IncomingMessage } from "http"
import { APIEvent } from "solid-start"
colors.enable()

View file

@ -0,0 +1,111 @@
import { and, eq, exists, ne } from "drizzle-orm"
import db from "~/drizzle"
import { games, user_games } from "~/drizzle/schemas/Tables"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { GamePropsSchema } from "~/lib/zodSchemas"
export const gameSelects = {
columns: {
id: true,
allowChat: true,
allowMarkDraw: true,
allowSpecials: true,
allowSpectators: true,
state: true,
},
with: {
gamePin: {
columns: {
pin: true,
},
},
users: {
columns: {
id: true,
index: true,
},
with: {
chats: {
columns: {
id: true,
event: true,
message: true,
createdAt: true,
},
},
moves: {
columns: {
index: true,
type: true,
x: true,
y: true,
orientation: true,
},
},
ships: {
columns: {
size: true,
variant: true,
x: true,
y: true,
orientation: true,
},
},
user: {
columns: {
id: true,
name: true,
},
},
},
},
},
} as const
export const getGameById = async (gameId: string) => {
return db.query.games.findFirst({
where: and(ne(games.state, "ended"), eq(games.id, gameId)),
...gameSelects,
})
}
export const getRunningGameToUser = async (userId: string) => {
return db.query.games.findFirst({
where: (game) =>
and(
ne(game.state, "ended"),
exists(
db
.select()
.from(user_games)
.where(
and(
eq(user_games.gameId, game.id),
eq(user_games.userId, userId),
),
),
),
),
...gameSelects,
})
}
export function composeBody(
gameDB: NonNullable<Awaited<ReturnType<typeof getRunningGameToUser>>>,
): GamePropsSchema {
const { gamePin, users, ...game } = gameDB
const mappedUsers = users.map(({ user, ...props }) => ({
...props,
...user,
}))
const composedUsers = {
0: mappedUsers.find((e) => e.index === 0) ?? null,
1: mappedUsers.find((e) => e.index === 1) ?? null,
}
const payload = {
game: game,
gamePin: gamePin?.pin ?? null,
users: composedUsers,
}
return getPayloadwithChecksum(payload)
}

View file

@ -1,4 +1,4 @@
import { APIEvent, json } from "solid-start"
import { APIEvent } from "@solidjs/start/server/types"
import { rejectionError } from "./errors"
import logging from "./logging"
@ -15,5 +15,7 @@ export default function sendError(
)
if ("name" in err) console.log("Sending Respons: " + err)
// If something went wrong, let the client know with status 500
return json(null, { status: "statusCode" in err ? err.statusCode : 500 })
return new Response(null, {
status: "statusCode" in err ? err.statusCode : 500,
})
}

View file

@ -1,4 +1,5 @@
import { APIEvent, json, redirect } from "solid-start/api"
import { redirect } from "@solidjs/router"
import { APIEvent } from "@solidjs/start/server/types"
import logging, { Logging } from "./logging"
export interface Result<T> {
@ -18,6 +19,11 @@ export default function sendResponse<T>(
return redirect(result.redirectUrl)
} else {
logging(result.message, result.type ?? ["debug"], request)
return json(result.body, { status: result.statusCode ?? 200 })
return new Response(JSON.stringify(result.body), {
status: result.statusCode ?? 200,
headers: {
"Content-Type": "application/json",
},
})
}
}

View file

@ -1,56 +0,0 @@
// @refresh reload
import "@fortawesome/fontawesome-svg-core/styles.css"
import { Suspense } from "solid-js"
import {
Body,
ErrorBoundary,
FileRoutes,
Head,
Html,
Link,
Meta,
Routes,
Scripts,
Title,
} from "solid-start"
import { SessionProvider } from "./hooks/useSession"
import "./styles/App.scss"
import "./styles/globals.scss"
import "./styles/grid.scss"
import "./styles/grid2.scss"
import "./styles/root.css"
export default function Root() {
return (
<Html lang="en">
<Head>
<Title>Leaky Ships</Title>
<Meta charset="utf-8" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Link rel="manifest" href="/manifest.json" />
<Link rel="icon" href="/favicon.ico" />
<Meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta name="theme-color" content="#000000" />
<Meta
name="description"
content="Battleship web app with react frontend and ASP .NET backend"
/>
<Link rel="apple-touch-icon" href="/logo192.png" />
</Head>
<Body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<Suspense fallback={<div>Loading</div>}>
<ErrorBoundary>
<SessionProvider>
<Routes>
<FileRoutes />
</Routes>
</SessionProvider>
</ErrorBoundary>
</Suspense>
<Scripts />
</Body>
</Html>
)
}

View file

@ -1,5 +1,5 @@
import { redirect } from "solid-start"
import { Navigate } from "@solidjs/router"
export function GET() {
return redirect("/")
export default function NOT_FOUND() {
return <Navigate href="/" />
}

View file

@ -1,6 +1,6 @@
import { getSession } from "@auth/solid-start"
import { APIEvent } from "@solidjs/start/server/types"
import { eq } from "drizzle-orm"
import { APIEvent } from "solid-start"
import { z } from "zod"
import db from "~/drizzle"
import { games } from "~/drizzle/schemas/Tables"

View file

@ -1,13 +1,17 @@
import { getSession } from "@auth/solid-start"
import { createId } from "@paralleldrive/cuid2"
import { APIEvent } from "@solidjs/start/server/types"
import { eq } from "drizzle-orm"
import { APIEvent } from "solid-start"
import db from "~/drizzle"
import { chats, gamepins, games, user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors"
import {
composeBody,
gameSelects,
getRunningGameToUser,
} from "~/lib/backend/processRes"
import sendResponse from "~/lib/backend/sendResponse"
import { authOptions } from "~/server/auth"
import { composeBody, gameSelects, getRunningGameToUser } from "./running"
export async function POST({ request }: APIEvent) {
const session = await getSession(request, authOptions)
@ -15,6 +19,8 @@ export async function POST({ request }: APIEvent) {
if (!session?.user) {
return sendResponse(request, rejectionErrors.unauthorized)
}
// @ts-ignore
const { email, id } = session.user
// Generate a random 4-digit code

View file

@ -1,16 +1,16 @@
import { getSession } from "@auth/solid-start"
import { createId } from "@paralleldrive/cuid2"
import { APIEvent } from "@solidjs/start/server/types"
import { and, eq } from "drizzle-orm"
import { APIEvent } from "solid-start"
import db from "~/drizzle"
import { user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors"
import getPinFromBody from "~/lib/backend/getPinFromBody"
import logging from "~/lib/backend/logging"
import { composeBody, gameSelects, getGameById } from "~/lib/backend/processRes"
import sendError from "~/lib/backend/sendError"
import sendResponse from "~/lib/backend/sendResponse"
import { authOptions } from "~/server/auth"
import { composeBody, gameSelects, getGameById } from "./running"
export async function POST({ request }: APIEvent) {
const session = await getSession(request, authOptions)
@ -20,6 +20,7 @@ export async function POST({ request }: APIEvent) {
return sendResponse(request, rejectionErrors.unauthorized)
}
// @ts-ignore
const { email, id } = session.user
try {

View file

@ -1,121 +1,14 @@
import { getSession } from "@auth/solid-start"
import { and, eq, exists, ne, notExists } from "drizzle-orm"
import { APIEvent } from "solid-start/api"
import { APIEvent } from "@solidjs/start/server/types"
import { eq, notExists } from "drizzle-orm"
import db from "~/drizzle"
import { games, user_games } from "~/drizzle/schemas/Tables"
import { rejectionErrors } from "~/lib/backend/errors"
import logging from "~/lib/backend/logging"
import { composeBody, getRunningGameToUser } from "~/lib/backend/processRes"
import sendResponse from "~/lib/backend/sendResponse"
import { getPayloadwithChecksum } from "~/lib/getPayloadwithChecksum"
import { GamePropsSchema } from "~/lib/zodSchemas"
import { authOptions } from "~/server/auth"
export const gameSelects = {
columns: {
id: true,
allowChat: true,
allowMarkDraw: true,
allowSpecials: true,
allowSpectators: true,
state: true,
},
with: {
gamePin: {
columns: {
pin: true,
},
},
users: {
columns: {
id: true,
index: true,
},
with: {
chats: {
columns: {
id: true,
event: true,
message: true,
createdAt: true,
},
},
moves: {
columns: {
index: true,
type: true,
x: true,
y: true,
orientation: true,
},
},
ships: {
columns: {
size: true,
variant: true,
x: true,
y: true,
orientation: true,
},
},
user: {
columns: {
id: true,
name: true,
},
},
},
},
},
} as const
export const getGameById = async (gameId: string) => {
return db.query.games.findFirst({
where: and(ne(games.state, "ended"), eq(games.id, gameId)),
...gameSelects,
})
}
export const getRunningGameToUser = async (userId: string) => {
return db.query.games.findFirst({
where: (game) =>
and(
ne(game.state, "ended"),
exists(
db
.select()
.from(user_games)
.where(
and(
eq(user_games.gameId, game.id),
eq(user_games.userId, userId),
),
),
),
),
...gameSelects,
})
}
export function composeBody(
gameDB: NonNullable<Awaited<ReturnType<typeof getRunningGameToUser>>>,
): GamePropsSchema {
const { gamePin, users, ...game } = gameDB
const mappedUsers = users.map(({ user, ...props }) => ({
...props,
...user,
}))
const composedUsers = {
0: mappedUsers.find((e) => e.index === 0) ?? null,
1: mappedUsers.find((e) => e.index === 1) ?? null,
}
const payload = {
game: game,
gamePin: gamePin?.pin ?? null,
users: composedUsers,
}
return getPayloadwithChecksum(payload)
}
export async function GET({ request }: APIEvent) {
const session = await getSession(request, authOptions)
@ -143,6 +36,7 @@ export async function GET({ request }: APIEvent) {
)
}
// @ts-ignore
const { email, id } = session.user
const game = await getRunningGameToUser(id)

View file

@ -1,10 +0,0 @@
import { getSession } from "@auth/solid-start"
import status from "http-status"
import { APIEvent, json } from "solid-start"
import { authOptions } from "~/server/auth"
export async function GET({ request }: APIEvent) {
const session = await getSession(request, authOptions)
if (session) return json(session)
else return new Response(null, { status: status["UNAUTHORIZED"] })
}

View file

@ -1,47 +1,45 @@
import { getSession } from "@auth/solid-start"
import { createId } from "@paralleldrive/cuid2"
import { APIEvent } from "@solidjs/start/server/types"
import colors from "colors"
import { and, eq } from "drizzle-orm"
import status from "http-status"
import { Server } from "socket.io"
import { APIEvent } from "solid-start"
import db from "~/drizzle"
import { games, moves, ships, user_games } from "~/drizzle/schemas/Tables"
import { SocketServer, sServer } from "~/interfaces/ApiSocket"
import { SocketWithIO, sServer } from "~/interfaces/ApiSocket"
import logging from "~/lib/backend/logging"
import { GamePropsSchema } from "~/lib/zodSchemas"
import { authOptions } from "~/server/auth"
import {
composeBody,
gameSelects,
getGameById,
getRunningGameToUser,
} from "./game/running"
} from "~/lib/backend/processRes"
import { GamePropsSchema } from "~/lib/zodSchemas"
import { authOptions } from "~/server/auth"
colors.enable()
export async function GET({
request,
httpServer,
}: APIEvent & { httpServer: SocketServer }) {
if (httpServer.io) {
export async function GET({ request, nativeEvent }: APIEvent) {
const socket = nativeEvent.node.res.socket as SocketWithIO
if (socket.server.io) {
logging("Socket is already running " + request.url, ["infoCyan"], request)
} else {
logging("Socket is initializing " + request.url, ["infoCyan"], request)
const io: sServer = new Server(httpServer, {
const io: sServer = new Server(socket.server, {
path: "/api/ws",
cors: {
origin: "https://leaky-ships.mal-noh.de",
origin: import.meta.env.VITE_AUTH_URL,
},
})
httpServer.io = io
socket.server.io = io
// io.use(authenticate)
io.use(async (socket, next) => {
const request = socket.request
try {
const url = process.env.AUTH_URL! + request.url
const url = import.meta.env.VITE_AUTH_URL! + request.url
const session = await getSession(
new Request(url, {
headers: request.headers as Record<string, string>,
@ -49,6 +47,8 @@ export async function GET({
authOptions,
)
if (!session) return next(new Error(status["401"]))
// @ts-ignore
socket.data.user = session.user
const game = await getRunningGameToUser(socket.data.user?.id ?? "")

View file

@ -1,4 +1,4 @@
import { Link, Meta, Title } from "solid-start"
import { Link, Meta, Title } from "@solidjs/meta"
import Gamefield from "~/components/Gamefield/Gamefield"
export default function Home() {

View file

@ -1,4 +1,4 @@
import { Link, Meta, Title } from "solid-start"
import { Link, Meta, Title } from "@solidjs/meta"
import Grid from "~/components/Grid"
export default function Home() {

View file

@ -1,4 +1,4 @@
import { Link, Meta, Title } from "solid-start"
import { Link, Meta, Title } from "@solidjs/meta"
import Grid2 from "~/components/Grid2"
export default function Home() {

View file

@ -1,4 +1,3 @@
import { A } from "solid-start"
import BurgerMenu from "~/components/BurgerMenu"
import Logo from "~/components/Logo"
@ -11,13 +10,13 @@ export default function Home() {
<div class="flex h-36 w-64 items-center justify-center overflow-hidden rounded-xl border-8 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]">
<video controls preload="metadata" src="/Regelwerk.mp4" />
</div>
<A
<a
id="start"
class="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"
href="/start"
>
START
</A>
</a>
</div>
</div>
)

View file

@ -1,8 +1,8 @@
import { signIn } from "@auth/solid-start/client"
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
import { useNavigate, useSearchParams } from "@solidjs/router"
import classNames from "classnames"
import { Show, createEffect, createSignal } from "solid-js"
import { A, useNavigate, useSearchParams } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useSession } from "~/hooks/useSession"
@ -55,7 +55,7 @@ function Login() {
if (session()) navigator("/signout")
})
const login = (provider: "email" | "azure-ad") =>
const login = (provider: "discord") =>
signIn(provider, { email: email(), callbackUrl: "/" })
return (
@ -85,7 +85,7 @@ function Login() {
onSubmit={(e) => {
e.preventDefault()
if (!email()) return
login("email")
// login("email")
}}
>
<label for="email" class="mx-2 text-lg">
@ -130,7 +130,7 @@ function Login() {
</a>
<button
id="microsoft"
onClick={() => login("azure-ad")}
onClick={() => login("discord")}
class="flex w-full justify-evenly rounded-lg border border-gray-400 bg-slate-100 px-5 py-3 text-black drop-shadow-md duration-300 hover:bg-slate-200"
>
<img
@ -147,14 +147,14 @@ function Login() {
<Show when={errorType}>
<hr class="mt-8 border-gray-400" />
<div class="flex flex-col items-center">
<A
<a
id="back"
href="/"
class="mt-10 rounded-lg border-2 border-gray-400 bg-gray-500 bg-opacity-75 px-16 py-2 text-white shadow-inner drop-shadow-md backdrop-blur-md transition-colors duration-300 hover:border-blue-600"
>
<FontAwesomeIcon icon={faLeftLong} />
<span class="mx-4 font-bold">Return</span>
</A>
</a>
</div>
</Show>
</div>

View file

@ -1,7 +1,7 @@
import { signOut } from "@auth/solid-start/client"
import { faLeftLong } from "@fortawesome/pro-solid-svg-icons"
import { useNavigate } from "@solidjs/router"
import { createEffect } from "solid-js"
import { useNavigate } from "solid-start"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import { useSession } from "~/hooks/useSession"

View file

@ -1,11 +1,7 @@
import { faEye, faLeftLong } from "@fortawesome/pro-regular-svg-icons"
import { faPlus, faUserPlus } from "@fortawesome/pro-solid-svg-icons"
import { GamePropsSchema } from "~/lib/zodSchemas"
// import OtpInput from "react-otp-input"
// import { Icons, toast } from "react-toastify"
import status from "http-status"
import { useNavigate, useSearchParams } from "@solidjs/router"
import { Show, createEffect, createSignal } from "solid-js"
import { A, useNavigate, useSearchParams } from "solid-start"
import BurgerMenu from "~/components/BurgerMenu"
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon"
import Logo from "~/components/Logo"
@ -15,43 +11,17 @@ import {
OptionDiv,
} from "~/components/OptionButton"
import { full } from "~/hooks/useGameProps"
import { useSession } from "~/hooks/useSession"
export function isAuthenticated(res: Response) {
switch (status[`${res.status}_CLASS`]) {
case status.classes.SUCCESSFUL:
case status.classes.REDIRECTION:
return res.json()
}
const resStatus = status[`${res.status}_CLASS`]
if (typeof resStatus !== "string") return
// toast(status[res.status], {
// position: "top-center",
// type: "info",
// theme: "colored",
// })
}
// const handleConfirmation = () => {
// const toastId = "confirm"
// toast.warn(
// <div id="toast-confirm">
// <h4>You are already in another round, do you want to:</h4>
// <button onClick={() => toast.dismiss(toastId)}>Join</button>
// or
// <button onClick={() => toast.dismiss(toastId)}>Leave</button>
// </div>,
// { autoClose: false, toastId },
// )
// }
import {
SessionProvider,
isAuthenticated,
useSession,
} from "~/hooks/useSession"
import { GamePropsSchema } from "~/lib/zodSchemas"
export default function Start() {
const [otp, setOtp] = createSignal("")
const navigator = useNavigate()
const { session } = useSession()
const [searchParams] = useSearchParams()
const query = () => {
@ -138,81 +108,83 @@ export default function Start() {
})
return (
<div class="h-full bg-theme">
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo />
<BurgerMenu />
<div class="flex flex-col items-center gap-4 rounded-xl border-4 border-black bg-grayish px-6 pb-6 pt-4 shadow-lg sm:mx-8 sm:px-12 sm:pb-12 sm:pt-8 md:w-full">
<div class="flex w-full justify-between">
<A
id="back"
class="self-start rounded-lg border-b-4 border-shield-gray bg-voidDark px-8 text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:px-14 sm:py-1 sm:text-5xl"
href="/"
>
<FontAwesomeIcon icon={faLeftLong} />
</A>
<Show when={!session()?.user?.id}>
<A
id="login"
class="self-start rounded-lg border-b-4 border-orange-500 bg-yellow-500 px-4 text-2xl active:border-b-0 active:border-t-4 sm:rounded-xl sm:px-9 sm:py-2 sm:text-4xl"
href="/signin"
<SessionProvider>
<div class="h-full bg-theme">
<div class="mx-auto flex h-full max-w-screen-md flex-col items-center justify-evenly">
<Logo />
<BurgerMenu />
<div class="flex flex-col items-center gap-4 rounded-xl border-4 border-black bg-grayish px-6 pb-6 pt-4 shadow-lg sm:mx-8 sm:px-12 sm:pb-12 sm:pt-8 md:w-full">
<div class="flex w-full justify-between">
<a
id="back"
class="self-start rounded-lg border-b-4 border-shield-gray bg-voidDark px-8 text-2xl text-grayish duration-100 active:border-b-0 active:border-t-4 sm:rounded-xl sm:px-14 sm:py-1 sm:text-5xl"
href="/"
>
Login
</A>
</Show>
</div>
<div class="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton
text="Raum erstellen"
callback={gameFetch}
icon={faPlus}
disabled={!session()}
/>
<Show
when={query().join && !!session()}
fallback={
<OptionAnchor
text="Raum beitreten"
href="?q=join"
icon={faUserPlus}
disabled={!session()}
/>
}
>
<OptionDiv icon={faUserPlus}>
<input
disabled={!session()}
value={otp()}
onInput={(e) =>
setOtp((otp) => {
const value = e.target.value
return /^\d{0,4}$/.test(value) ? value : otp
})
}
/>
</OptionDiv>
</Show>
<Show
when={query().watch}
fallback={
<OptionAnchor text="Zuschauen" icon={faEye} href="?q=watch" />
}
>
<OptionDiv icon={faEye}>
<input
value={otp()}
onInput={(e) =>
setOtp((otp) => {
const value = e.target.value
return /^\d{0,4}$/.test(value) ? value : otp
})
}
/>
</OptionDiv>
</Show>
<FontAwesomeIcon icon={faLeftLong} />
</a>
<Show when={!session()?.user?.id}>
<a
id="login"
class="self-start rounded-lg border-b-4 border-orange-500 bg-yellow-500 px-4 text-2xl active:border-b-0 active:border-t-4 sm:rounded-xl sm:px-9 sm:py-2 sm:text-4xl"
href="/signin"
>
Login
</a>
</Show>
</div>
<div class="flex flex-col items-center gap-6 sm:gap-12">
<OptionButton
text="Raum erstellen"
callback={gameFetch}
icon={faPlus}
disabled={!session()}
/>
<Show
when={query().join && !!session()}
fallback={
<OptionAnchor
text="Raum beitreten"
href="?q=join"
icon={faUserPlus}
disabled={!session()}
/>
}
>
<OptionDiv icon={faUserPlus}>
<input
disabled={!session()}
value={otp()}
onInput={(e) =>
setOtp((otp) => {
const value = e.target.value
return /^\d{0,4}$/.test(value) ? value : otp
})
}
/>
</OptionDiv>
</Show>
<Show
when={query().watch}
fallback={
<OptionAnchor text="Zuschauen" icon={faEye} href="?q=watch" />
}
>
<OptionDiv icon={faEye}>
<input
value={otp()}
onInput={(e) =>
setOtp((otp) => {
const value = e.target.value
return /^\d{0,4}$/.test(value) ? value : otp
})
}
/>
</OptionDiv>
</Show>
</div>
</div>
</div>
</div>
</div>
</SessionProvider>
)
}

View file

@ -1,5 +1,4 @@
import AzureADProvider from "@auth/core/providers/azure-ad"
import EmailProvider from "@auth/core/providers/email"
import Discord from "@auth/core/providers/discord"
import { DrizzleAdapter } from "@auth/drizzle-adapter"
import { type SolidAuthConfig } from "@auth/solid-start"
import {
@ -20,19 +19,14 @@ const customConfig: Config = {
export const authOptions: SolidAuthConfig = {
providers: [
// @ts-expect-error Types are wrong
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
}),
AzureADProvider({
clientId: process.env.AZURE_AD_CLIENT_ID ?? "",
clientSecret: process.env.AZURE_AD_CLIENT_SECRET ?? "",
tenantId: process.env.AZURE_AD_TENANT_ID,
// @ts-ignore
Discord({
clientId: import.meta.env.VITE_DISCORD_CLIENT_ID,
clientSecret: import.meta.env.VITE_DISCORD_CLIENT_SECRET,
}),
],
adapter: DrizzleAdapter(db),
secret: process.env.AUTH_SECRET,
secret: import.meta.env.VITE_AUTH_SECRET,
callbacks: {
signIn: ({ user, account }) => {
// Custom signIn callback to add username to email provider
@ -43,6 +37,7 @@ export const authOptions: SolidAuthConfig = {
},
session: ({ session, user }) => {
if (session?.user) {
// @ts-ignore
session.user.id = user.id
}
return session

16
leaky-ships/src/types/env.d.ts vendored Normal file
View file

@ -0,0 +1,16 @@
/// <reference types="vinxi/client" />
interface ImportMetaEnv {
readonly VITE_DISCORD_CLIENT_ID: string
readonly VITE_DISCORD_CLIENT_SECRET: string
readonly VITE_AUTH_SECRET: string
readonly VITE_AUTH_URL: string | undefined
readonly VITE_DATABASE_URL: string
}
// eslint-disable-next-line no-unused-vars
interface ImportMeta {
readonly env: ImportMetaEnv
}

View file

@ -1,3 +0,0 @@
{
"include": [".eslintrc.cjs"]
}

View file

@ -1,15 +1,17 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node",
"jsxImportSource": "solid-js",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "preserve",
"jsxImportSource": "solid-js",
"allowJs": true,
"strict": true,
"types": ["solid-start/env"],
"baseUrl": "./",
"noEmit": true,
"types": ["vinxi/client"],
"isolatedModules": true,
"paths": {
"~/*": ["./src/*"]
}

View file

@ -1,10 +1,3 @@
import solid from "solid-start/vite"
import { defineConfig } from "vite"
import { defineConfig } from "@solidjs/start/config"
export default defineConfig({
plugins: [solid({ ssr: false })],
server: {
host: "0.0.0.0",
strictPort: true,
},
})
export default defineConfig({})