Delete redundants parts

This commit is contained in:
Kalle (Sendou) 2021-11-09 23:32:40 +02:00
parent 1c05643de6
commit e1c05939cb
20 changed files with 3 additions and 2348 deletions

View File

@ -27,17 +27,6 @@ You might be able to skip steps 2-4 and use a few pages but most pages need a se
## .env
### /server
| Name | Description | Required |
| --------------------- | ----------------------------------------------------------------------------------------------------------- | -------- |
| DATABASE_URL | Database connection string. For example mine is currently `postgresql://sendou@localhost:5432/sendou_ink_3` | Yes |
| DISCORD_CLIENT_ID | Used for auth. Make an application on [Discord](https://discord.com/developers/applications) | No |
| DISCORD_CLIENT_SECRET | See above. | No |
| DISCORD_CALLBACK_URL | See above. | No |
| FRONTEND_URL | Where frontend is located. Cell | Yes |
| NODE_ENV | `development` when developing, `production` in production | Yes |
### /frontend
| Name | Description | Required |
@ -63,23 +52,6 @@ frontend/
└── utils/ frontend specific utility methods, constants etc.
```
### /server
This folder contains backend specific code.
```
server/
├── index.ts/ entry point
├── routes/ route handling logic (controllers)
├── middleware/ middlewares that are called before controllers
├── services/ methods that talk to the database
├── core/ business logic
└── prisma/
├── migrations/ migrations
├── schema.prisma database models
└── seed.ts seeding script
```
### /shared
Utilities, constants etc. that are shared between frontend and backend.

View File

@ -1,6 +0,0 @@
import useSWR from "swr";
import { Serialized } from "utils/types";
export function useMySWR<T>(key: string | null) {
return useSWR<Serialized<T>>(key);
}

View File

@ -14,7 +14,6 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
"swr": "^1.0.1",
"urql": "^2.0.5"
},
"devDependencies": {
@ -5863,14 +5862,6 @@
"node": ">= 0.6.0"
}
},
"node_modules/dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug==",
"engines": {
"node": ">=6"
}
},
"node_modules/des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@ -14512,17 +14503,6 @@
"tslib": "^2.0.3"
}
},
"node_modules/swr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/swr/-/swr-1.0.1.tgz",
"integrity": "sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==",
"dependencies": {
"dequal": "2.0.2"
},
"peerDependencies": {
"react": "^16.11.0 || ^17.0.0"
}
},
"node_modules/symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",
@ -20708,11 +20688,6 @@
"integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==",
"dev": true
},
"dequal": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.2.tgz",
"integrity": "sha512-q9K8BlJVxK7hQYqa6XISGmBZbtQQWVXSrRrWreHC94rMt1QL/Impruc+7p2CYSYuVIUr+YCt6hjrs1kkdJRTug=="
},
"des.js": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz",
@ -27493,14 +27468,6 @@
"tslib": "^2.0.3"
}
},
"swr": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/swr/-/swr-1.0.1.tgz",
"integrity": "sha512-EPQAxSjoD4IaM49rpRHK0q+/NzcwoT8c0/Ylu/u3/6mFj/CWnQVjNJ0MV2Iuw/U+EJSd2TX5czdAwKPYZIG0YA==",
"requires": {
"dequal": "2.0.2"
}
},
"symbol-observable": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz",

View File

@ -17,7 +17,6 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-icons": "^4.3.1",
"swr": "^1.0.1",
"urql": "^2.0.5"
},
"devDependencies": {

View File

@ -3,7 +3,6 @@ import "./_app.css";
import { AppProps } from "next/app";
import Head from "next/head";
import { SWRConfig } from "swr";
import { Layout } from "components/layout/Layout";
import { globalCss } from "stitches.config";
import { Provider, createClient } from "urql";
@ -42,18 +41,9 @@ export default function App(props: AppProps) {
/>
</Head>
<Provider value={client}>
<SWRConfig
value={{
fetcher: (resource, init) =>
fetch(process.env.NEXT_PUBLIC_BACKEND_URL + resource, init).then(
(res) => res.json()
),
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
</SWRConfig>
<Layout>
<Component {...pageProps} />
</Layout>
</Provider>
</>
);

View File

@ -3,7 +3,6 @@
"version": "1.0.0",
"private": true,
"workspaces": [
"server",
"frontend",
"api",
"shared"

View File

@ -1,6 +0,0 @@
DATABASE_URL=
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
DISCORD_CALLBACK_URL=
FRONTEND_URL=http://localhost:3000
NODE_ENV=development

View File

@ -1,94 +0,0 @@
import { config } from "dotenv";
config();
import { App } from "@tinyhttp/app";
import { logger } from "@tinyhttp/logger";
import { cors } from "@tinyhttp/cors";
import passport from "passport";
import { Strategy as DiscordStrategy } from "passport-discord";
import routes from "./routes/index";
import { findUserById, upsertUser } from "./services/user";
const app = new App();
const PORT = 3001;
validateEnvVars();
passport.use(
new DiscordStrategy(
{
clientID: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
callbackURL: process.env.DISCORD_CALLBACK_URL!,
scope: ["identify", "connections"],
},
function (_accessToken, refreshToken, loggedInUser, cb) {
upsertUser({ loggedInUser, refreshToken: refreshToken })
.then((user) => {
return cb(null, user);
})
.catch((err) => cb(err));
}
)
);
passport.serializeUser((user, done) => {
// @ts-expect-error - Mismatch of types but seems to work fine
const id = user?.id;
if (typeof id !== "number") {
return done(new Error("typeof id is not number"));
}
done(null, id);
});
passport.deserializeUser(async (id, done) => {
if (typeof id !== "number") {
return done(new Error("typeof id is not number"));
}
const user = await findUserById(id);
done(null, user);
});
app
.use(logger())
.use(cors({ origin: process.env.FRONTEND_URL }))
// @ts-expect-error - Mismatch of types but seems to work fine
.use(passport.initialize())
.get("/auth/discord", passport.authenticate("discord"))
.get(
"/auth/discord/callback",
passport.authenticate("discord", {
failureRedirect: "/login",
successRedirect: "/",
})
)
.use(routes)
.listen(PORT, () =>
console.log(`Server ready at: https://localhost:${PORT}`)
);
function validateEnvVars() {
if (!process.env.FRONTEND_URL) {
throw new Error("Missing env var for setting cors");
}
const logInEnvVars = [
"DISCORD_CLIENT_ID",
"DISCORD_CLIENT_SECRET",
"DISCORD_CALLBACK_URL",
].filter((envVar) => !process.env[envVar]);
if (logInEnvVars.length === 0) return;
if (process.env.NODE_ENV === "development") {
console.warn(
"Missing env vars for testing logging in:",
logInEnvVars.join(", ")
);
} else {
throw new Error("Missing env vars for logging in");
}
}

1685
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,33 +0,0 @@
{
"name": "@sendou.ink/server",
"version": "1.0.0",
"scripts": {
"dev": "tsm index.ts",
"migration:create": "npx prisma migrate dev --create-only",
"migration:apply:dev": "npx prisma migrate dev",
"migration:apply:prod": "npx prisma migrate deploy",
"seed": "npx prisma migrate reset"
},
"dependencies": {
"@prisma/client": "^3.3.0",
"@tinyhttp/app": "^2.0.11",
"@tinyhttp/logger": "^1.3.0",
"dotenv": "^10.0.0",
"passport": "^0.5.0",
"passport-discord": "^0.1.4"
},
"devDependencies": {
"@tinyhttp/cors": "^2.0.0",
"@types/node": "^16.11.4",
"@types/passport": "^1.0.7",
"@types/passport-discord": "^0.1.5",
"prisma": "^3.3.0",
"ts-node": "^10.4.0",
"tsm": "^2.1.2",
"typescript": "^4.4.4"
},
"type": "module",
"prisma": {
"seed": "node --loader ts-node/esm prisma/seed.ts"
}
}

View File

@ -1,6 +0,0 @@
import pkg from "@prisma/client";
const { PrismaClient } = pkg;
const prisma = new PrismaClient();
export default prisma;

View File

@ -1,92 +0,0 @@
-- CreateEnum
CREATE TYPE "Mode" AS ENUM ('TW', 'SZ', 'TC', 'RM', 'CB');
-- CreateTable
CREATE TABLE "User" (
"id" SERIAL NOT NULL,
"discordId" TEXT NOT NULL,
"discordName" TEXT NOT NULL,
"discordDiscriminator" TEXT NOT NULL,
"discordAvatar" TEXT,
"discordRefreshToken" TEXT NOT NULL,
"twitch" TEXT,
"twitter" TEXT,
"youtubeId" TEXT,
"youtubeName" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Organization" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"nameForUrl" TEXT NOT NULL,
"ownerId" INTEGER NOT NULL,
"discordInvite" TEXT NOT NULL,
"twitter" TEXT,
CONSTRAINT "Organization_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Tournament" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"nameForUrl" TEXT NOT NULL,
"description" TEXT,
"startTime" TIMESTAMP(3) NOT NULL,
"checkInTime" TIMESTAMP(3) NOT NULL,
"bannerBackground" TEXT NOT NULL,
"bannerTextHSLArgs" TEXT NOT NULL,
"organizerId" INTEGER NOT NULL,
CONSTRAINT "Tournament_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Stage" (
"id" SERIAL NOT NULL,
"name" TEXT NOT NULL,
"mode" "Mode" NOT NULL,
CONSTRAINT "Stage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_StageToTournament" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "User_discordId_key" ON "User"("discordId");
-- CreateIndex
CREATE UNIQUE INDEX "Organization_nameForUrl_key" ON "Organization"("nameForUrl");
-- CreateIndex
CREATE UNIQUE INDEX "Organization_ownerId_key" ON "Organization"("ownerId");
-- CreateIndex
CREATE UNIQUE INDEX "Tournament_nameForUrl_organizerId_key" ON "Tournament"("nameForUrl", "organizerId");
-- CreateIndex
CREATE UNIQUE INDEX "_StageToTournament_AB_unique" ON "_StageToTournament"("A", "B");
-- CreateIndex
CREATE INDEX "_StageToTournament_B_index" ON "_StageToTournament"("B");
-- AddForeignKey
ALTER TABLE "Organization" ADD CONSTRAINT "Organization_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "Tournament" ADD CONSTRAINT "Tournament_organizerId_fkey" FOREIGN KEY ("organizerId") REFERENCES "Organization"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StageToTournament" ADD FOREIGN KEY ("A") REFERENCES "Stage"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_StageToTournament" ADD FOREIGN KEY ("B") REFERENCES "Tournament"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "postgresql"

View File

@ -1,74 +0,0 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
discordId String @unique
discordName String
discordDiscriminator String
discordAvatar String?
discordRefreshToken String
twitch String?
twitter String?
youtubeId String?
youtubeName String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
ownedOrganization Organization?
}
model Organization {
id Int @id @default(autoincrement())
name String
/// Name in lower case to show in URL
nameForUrl String @unique
ownerId Int @unique
owner User @relation(fields: [ownerId], references: [id])
discordInvite String
twitter String?
tournaments Tournament[]
}
model Tournament {
id Int @id @default(autoincrement())
name String
/// Name in lower case to show in URL
nameForUrl String
description String?
startTime DateTime
checkInTime DateTime
/// CSS for tournament banner's background value
bannerBackground String
/// CSS for tournament banner's color value
bannerTextHSLArgs String
mapPool Stage[]
organizerId Int
organizer Organization @relation(fields: [organizerId], references: [id])
// There might be duplicate nameForUrl's but inside an organization they're unique
@@unique([nameForUrl, organizerId])
}
enum Mode {
TW
SZ
TC
RM
CB
}
model Stage {
id Int @id @default(autoincrement())
name String
mode Mode
tournamentMapPools Tournament[]
}

View File

@ -1,104 +0,0 @@
import pkg from "@prisma/client";
const { PrismaClient } = pkg;
import { stages as stageList } from "@sendou-ink/shared/constants";
const prisma = new PrismaClient();
async function main() {
const user = await users();
const organization = await organizations(user.id);
const tournament = await tournaments(organization.id);
await stages();
await tournamentAddMaps(tournament.id);
}
async function users() {
return prisma.user.create({
data: {
discordDiscriminator: "4059",
discordId: "79237403620945920",
discordName: "Sendou",
discordRefreshToken: "none",
twitch: "Sendou",
youtubeId: "UCWbJLXByvsfQvTcR4HLPs5Q",
youtubeName: "Sendou",
discordAvatar: "fcfd65a3bea598905abb9ca25296816b",
twitter: "sendouc",
},
});
}
async function organizations(userId: number) {
return prisma.organization.create({
data: {
name: "Sendou's Tournaments",
discordInvite: "sendou",
nameForUrl: "sendou",
twitter: "sendouc",
ownerId: userId,
},
});
}
const modesList = ["TW", "SZ", "TC", "RM", "CB"] as const;
async function tournaments(organizationId: number) {
return prisma.tournament.create({
data: {
bannerBackground: "linear-gradient(to bottom, #9796f0, #fbc7d4)",
bannerTextHSLArgs: "231, 9%, 16%",
checkInTime: new Date(2025, 11, 17, 11),
startTime: new Date(2025, 11, 17, 12),
name: "In The Zone X",
nameForUrl: "in-the-zone-x",
organizerId: organizationId,
description: "In The Zone eXtreme",
},
});
}
function getRandomInt(maxInclusive: number) {
let result = -1;
while (result < 24) {
result = Math.floor(Math.random() * maxInclusive) + 1;
}
return result;
}
// TODO: why this can't be done while creating?
async function tournamentAddMaps(id: number) {
const ids = Array.from(
new Set(new Array(24).fill(null).map(() => ({ id: getRandomInt(115) })))
);
return prisma.tournament.update({
where: { id },
data: {
mapPool: {
connect: ids,
},
},
});
}
async function stages() {
return prisma.stage.createMany({
data: modesList.flatMap((mode) => {
return stageList.map((name) => {
return {
name,
mode,
};
});
}),
});
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@ -1,8 +0,0 @@
import { App } from "@tinyhttp/app";
import tournament from "./tournaments.routes";
const routes = new App();
routes.use("/tournaments", tournament);
export default routes;

View File

@ -1,20 +0,0 @@
import { App } from "@tinyhttp/app";
import { findTournamentByNameForUrl } from "../services/tournament";
import type { GetTournamentByOrganizationAndName } from "@sendou-ink/api";
const app = new App();
app.get("/:organization/:tournament", async (req, res) => {
const { organization, tournament } = req.params;
const tournamentFromDB: GetTournamentByOrganizationAndName | undefined =
await findTournamentByNameForUrl({
organizationNameForUrl: organization,
tournamentNameForUrl: tournament,
});
if (!tournamentFromDB) return res.sendStatus(404);
res.json(tournamentFromDB);
});
export default app;

View File

@ -1,61 +0,0 @@
import prisma from "../prisma/client";
export async function findTournamentByNameForUrl({
organizationNameForUrl,
tournamentNameForUrl,
}: {
organizationNameForUrl: string;
tournamentNameForUrl: string;
}) {
const tournaments = await prisma.tournament.findMany({
where: {
nameForUrl: tournamentNameForUrl.toLowerCase(),
},
select: {
name: true,
description: true,
startTime: true,
checkInTime: true,
bannerBackground: true,
bannerTextHSLArgs: true,
organizer: {
select: {
name: true,
discordInvite: true,
twitter: true,
nameForUrl: true,
},
},
mapPool: {
select: {
mode: true,
name: true,
},
},
},
});
const result = tournaments.find(
(tournament) =>
tournament.organizer.nameForUrl === organizationNameForUrl.toLowerCase()
);
if (!result) return result;
result.organizer.twitter = twitterToUrl(result.organizer.twitter);
result.organizer.discordInvite = discordInviteToUrl(
result.organizer.discordInvite
);
return result;
}
function twitterToUrl(twitter: string | null) {
if (!twitter) return twitter;
return `https://twitter.com/${twitter}`;
}
function discordInviteToUrl(discordInvite: string) {
return `https://discord.com/invite/${discordInvite}`;
}

View File

@ -1,64 +0,0 @@
import prisma from "../prisma/client";
import type { Strategy as DiscordStrategy } from "passport-discord";
export async function upsertUser({
loggedInUser,
refreshToken,
}: {
loggedInUser: DiscordStrategy.Profile;
refreshToken: string;
}) {
return prisma.user.upsert({
create: {
discordId: loggedInUser.id,
discordName: loggedInUser.username,
discordDiscriminator: loggedInUser.discriminator,
discordAvatar: loggedInUser.avatar,
discordRefreshToken: refreshToken,
...parseConnections(loggedInUser.connections),
},
update: {
discordName: loggedInUser.username,
discordDiscriminator: loggedInUser.discriminator,
discordAvatar: loggedInUser.avatar,
},
where: {
discordId: loggedInUser.id,
},
});
}
function parseConnections(
connections: DiscordStrategy.ConnectionInfo[] | undefined
) {
if (!connections) return null;
const result: {
twitch?: string;
twitter?: string;
youtubeId?: string;
youtubeName?: string;
} = {};
for (const connection of connections) {
if (connection.visibility !== 1 || !connection.verified) continue;
switch (connection.type) {
case "twitch":
result.twitch = connection.name;
break;
case "twitter":
result.twitter = connection.name;
break;
case "youtube":
result.youtubeId = connection.id;
result.youtubeName = connection.name;
}
}
return result;
}
export function findUserById(id: number) {
return prisma.user.findUnique({ where: { id } });
}

View File

@ -1,16 +0,0 @@
{
"compilerOptions": {
"target": "ES6",
"module": "ESNext",
"sourceMap": true,
"outDir": "dist",
"strict": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"lib": ["esnext"],
"esModuleInterop": true,
"moduleResolution": "node",
"baseUrl": "."
}
}