add frontend and swap whole stack to sveltekit/nodejs
This commit is contained in:
28
src/app.css
Normal file
28
src/app.css
Normal file
@ -0,0 +1,28 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
@font-face {
|
||||
font-family: 'Minecraft';
|
||||
src: url('/fonts/MinecraftRegular.otf') format('opentype');
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto';
|
||||
src: url('/fonts/Roboto-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
html {
|
||||
@apply font-roboto;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-minecraft;
|
||||
}
|
||||
}
|
12
src/app.d.ts
vendored
Normal file
12
src/app.d.ts
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
13
src/app.html
Normal file
13
src/app.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
4
src/hooks.server.ts
Normal file
4
src/hooks.server.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import { sequelize } from '$lib/server/database';
|
||||
|
||||
// make sure that the database and tables exist
|
||||
await sequelize.sync();
|
61
src/lib/components/Countdown/Countdown.svelte
Normal file
61
src/lib/components/Countdown/Countdown.svelte
Normal file
@ -0,0 +1,61 @@
|
||||
<script lang="ts">
|
||||
import { onDestroy } from 'svelte';
|
||||
|
||||
// start date in milliseconds. if undefined, start will be Date.now
|
||||
export let start: number | undefined = undefined;
|
||||
// end date in milliseconds
|
||||
export let end: number;
|
||||
|
||||
function getUntil(): [number, number, number, number] {
|
||||
let diff = (end - (start || Date.now())) / 1000;
|
||||
|
||||
return [
|
||||
Math.floor(diff / (60 * 60 * 24)),
|
||||
Math.floor((diff % (60 * 60 * 24)) / (60 * 60)),
|
||||
Math.floor((diff % (60 * 60)) / 60),
|
||||
Math.floor(diff % 60)
|
||||
];
|
||||
}
|
||||
|
||||
let [days, hours, minutes, seconds] = getUntil();
|
||||
let intervalId = setInterval(() => {
|
||||
[days, hours, minutes, seconds] = getUntil();
|
||||
if (start) start += 1000;
|
||||
}, 1000);
|
||||
|
||||
onDestroy(() => clearInterval(intervalId));
|
||||
</script>
|
||||
|
||||
<div class="grid grid-flow-col gap-5 text-center auto-cols-max">
|
||||
<div class="flex flex-col p-2 bg-neutral rounded-box text-neutral-content">
|
||||
<span class="countdown font-mono text-5xl">
|
||||
<span style="--value:{days};" />
|
||||
</span>
|
||||
Tage
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-neutral rounded-box text-neutral-content">
|
||||
<span class="countdown font-mono text-5xl">
|
||||
<span style="--value:{hours};" />
|
||||
</span>
|
||||
Stunden
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-neutral rounded-box text-neutral-content">
|
||||
<span class="countdown font-mono text-5xl">
|
||||
<span style="--value:{minutes};" />
|
||||
</span>
|
||||
Minuten
|
||||
</div>
|
||||
<div class="flex flex-col p-2 bg-neutral rounded-box text-neutral-content">
|
||||
<span class="countdown font-mono text-5xl">
|
||||
<span style="--value:{seconds};" />
|
||||
</span>
|
||||
Sekunden
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Set a custom content for the countdown before selector as it only supports numbers up to 99 */
|
||||
.countdown > ::before {
|
||||
content: '00\A 01\A 02\A 03\A 04\A 05\A 06\A 07\A 08\A 09\A 10\A 11\A 12\A 13\A 14\A 15\A 16\A 17\A 18\A 19\A 20\A 21\A 22\A 23\A 24\A 25\A 26\A 27\A 28\A 29\A 30\A 31\A 32\A 33\A 34\A 35\A 36\A 37\A 38\A 39\A 40\A 41\A 42\A 43\A 44\A 45\A 46\A 47\A 48\A 49\A 50\A 51\A 52\A 53\A 54\A 55\A 56\A 57\A 58\A 59\A 60\A 61\A 62\A 63\A 64\A 65\A 66\A 67\A 68\A 69\A 70\A 71\A 72\A 73\A 74\A 75\A 76\A 77\A 78\A 79\A 80\A 81\A 82\A 83\A 84\A 85\A 86\A 87\A 88\A 89\A 90\A 91\A 92\A 93\A 94\A 95\A 96\A 97\A 98\A 99\A 100\A 101\A 102\A 103\A 104\A 105\A 106\A 107\A 108\A 109\A 110\A 111\A 112\A 113\A 114\A 115\A 116\A 117\A 118\A 119\A 120\A 121\A 122\A 123\A 124\A 125\A 126\A 127\A 128\A 129\A 130\A 131\A 132\A 133\A 134\A 135\A 136\A 137\A 138\A 139\A 140\A 141\A 142\A 143\A 144\A 145\A 146\A 147\A 148\A 149\A 150\A 151\A 152\A 153\A 154\A 155\A 156\A 157\A 158\A 159\A 160\A 161\A 162\A 163\A 164\A 165\A 166\A 167\A 168\A 169\A 170\A 171\A 172\A 173\A 174\A 175\A 176\A 177\A 178\A 179\A 180\A 181\A 182\A 183\A 184\A 185\A 186\A 187\A 188\A 189\A 190\A 191\A 192\A 193\A 194\A 195\A 196\A 197\A 198\A 199\A 200\A 201\A 202\A 203\A 204\A 205\A 206\A 207\A 208\A 209\A 210\A 211\A 212\A 213\A 214\A 215\A 216\A 217\A 218\A 219\A 220\A 221\A 222\A 223\A 224\A 225\A 226\A 227\A 228\A 229\A 230\A 231\A 232\A 233\A 234\A 235\A 236\A 237\A 238\A 239\A 240\A 241\A 242\A 243\A 244\A 245\A 246\A 247\A 248\A 249\A 250\A 251\A 252\A 253\A 254\A 255\A 256\A 257\A 258\A 259\A 260\A 261\A 262\A 263\A 264\A 265\A 266\A 267\A 268\A 269\A 270\A 271\A 272\A 273\A 274\A 275\A 276\A 277\A 278\A 279\A 280\A 281\A 282\A 283\A 284\A 285\A 286\A 287\A 288\A 289\A 290\A 291\A 292\A 293\A 294\A 295\A 296\A 297\A 298\A 299\A 300\A 301\A 302\A 303\A 304\A 305\A 306\A 307\A 308\A 309\A 310\A 311\A 312\A 313\A 314\A 315\A 316\A 317\A 318\A 319\A 320\A 321\A 322\A 323\A 324\A 325\A 326\A 327\A 328\A 329\A 330\A 331\A 332\A 333\A 334\A 335\A 336\A 337\A 338\A 339\A 340\A 341\A 342\A 343\A 344\A 345\A 346\A 347\A 348\A 349\A 350\A 351\A 352\A 353\A 354\A 355\A 356\A 357\A 358\A 359\A 360\A 361\A 362\A 363\A 364\A';
|
||||
}
|
||||
</style>
|
72
src/lib/components/Input/Input.svelte
Normal file
72
src/lib/components/Input/Input.svelte
Normal file
@ -0,0 +1,72 @@
|
||||
<svelte:options accessors={true} />
|
||||
|
||||
<script lang="ts">
|
||||
import { IconSolid } from 'svelte-heros-v2';
|
||||
|
||||
export let id: string;
|
||||
export let name: string | null = null;
|
||||
export let type: string;
|
||||
export let value: string | null = null;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
|
||||
export let inputElement: HTMLInputElement | undefined = undefined;
|
||||
|
||||
let initialType = type;
|
||||
</script>
|
||||
|
||||
<!-- the cursor-not-allowed class must be set here because a disabled button does not respect the 'cursor' css property -->
|
||||
<div class={type === 'submit' && disabled ? 'cursor-not-allowed' : ''}>
|
||||
{#if type === 'submit'}
|
||||
<input class="btn" {id} type="submit" {value} {disabled} bind:this={inputElement} />
|
||||
{:else}
|
||||
<div>
|
||||
{#if $$slots.label}
|
||||
<label class="label" for={id}>
|
||||
<span class="label-text">
|
||||
<slot name="label" />
|
||||
{#if required}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
<div class="flex items-center">
|
||||
<input
|
||||
class={type === 'checkbox'
|
||||
? 'checkbox'
|
||||
: `input input-bordered w-[100%] sm:max-w-[16rem] ${
|
||||
initialType === 'password' ? 'pr-11' : ''
|
||||
}`}
|
||||
{id}
|
||||
{name}
|
||||
{type}
|
||||
{value}
|
||||
{required}
|
||||
{disabled}
|
||||
bind:this={inputElement}
|
||||
/>
|
||||
{#if initialType === 'password'}
|
||||
<button
|
||||
class="relative right-9"
|
||||
type="button"
|
||||
on:click={() => {
|
||||
type = type === 'password' ? 'text' : 'password';
|
||||
}}
|
||||
>
|
||||
{#if type === 'password'}
|
||||
<IconSolid name="eye-slash-solid" />
|
||||
{:else}
|
||||
<IconSolid name="eye-solid" />
|
||||
{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if $$slots.notice}
|
||||
<label class="label" for={id}>
|
||||
<span class="label-text-alt"><slot name="notice" /></span>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
37
src/lib/components/Input/Select.svelte
Normal file
37
src/lib/components/Input/Select.svelte
Normal file
@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
export let id: string;
|
||||
export let name: string | null = null;
|
||||
export let value: string;
|
||||
export let label: string | null = null;
|
||||
export let notice: string | null = null;
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label}
|
||||
<label class="label" for={id}>
|
||||
<span class="label-text">
|
||||
{label}
|
||||
{#if required}
|
||||
<span class="text-red-700">*</span>
|
||||
{/if}
|
||||
</span>
|
||||
</label>
|
||||
{/if}
|
||||
<select
|
||||
class="input input-bordered w-[100%] sm:max-w-[16rem]"
|
||||
{id}
|
||||
{name}
|
||||
{required}
|
||||
{disabled}
|
||||
bind:value
|
||||
>
|
||||
<slot />
|
||||
</select>
|
||||
{#if notice}
|
||||
<label class="label" for={id}>
|
||||
<span class="label-text-alt">{notice}</span>
|
||||
</label>
|
||||
{/if}
|
||||
</div>
|
1
src/lib/index.ts
Normal file
1
src/lib/index.ts
Normal file
@ -0,0 +1 @@
|
||||
// place files you want to import through the `$lib` alias in this folder.
|
37
src/lib/server/database.ts
Normal file
37
src/lib/server/database.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { DataTypes, Sequelize } from 'sequelize';
|
||||
import { DATABASE_URI } from '$env/static/private';
|
||||
import { dev } from '$app/environment';
|
||||
|
||||
export const sequelize = new Sequelize(DATABASE_URI, {
|
||||
// only log sql queries in dev mode
|
||||
logging: dev ? console.log : false
|
||||
});
|
||||
|
||||
export const User = sequelize.define('user', {
|
||||
firstname: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
lastname: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
birthday: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: false
|
||||
},
|
||||
telephone: DataTypes.STRING,
|
||||
username: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false
|
||||
},
|
||||
playertype: {
|
||||
type: DataTypes.ENUM('java', 'bedrock', 'cracked'),
|
||||
allowNull: false
|
||||
},
|
||||
password: DataTypes.TEXT,
|
||||
uuid: {
|
||||
type: DataTypes.UUIDV4,
|
||||
allowNull: false
|
||||
}
|
||||
});
|
27
src/lib/server/minecraft.test.ts
Normal file
27
src/lib/server/minecraft.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { getBedrockUuid, getCrackedUuid, getJavaUuid } from '$lib/server/minecraft';
|
||||
|
||||
describe('java username', () => {
|
||||
test('is valid', async () => {
|
||||
expect(getJavaUuid('bytedream')).resolves.toBe('9aa026cc-b5dc-4357-a319-ab668101af0d');
|
||||
});
|
||||
test('is invalid', async () => {
|
||||
expect(getJavaUuid('57eoTQaLYchmETrTsWnNZXtZh')).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('bedrock username', () => {
|
||||
test('is valid', async () => {
|
||||
expect(getBedrockUuid('bytedream')).resolves.toBe('00000000-0000-0000-0009-01F5BF975D37');
|
||||
});
|
||||
test('is invalid', async () => {
|
||||
expect(getBedrockUuid('57eoTQaLYchmETrTsWnNZXtZh')).rejects.toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cracked username', () => {
|
||||
// every username can be converted to an uuid so every user id automatically valid
|
||||
test('is valid', () => {
|
||||
expect(getCrackedUuid('bytedream')).toBe('88de3863-bf47-30f9-a7f4-ab6134feb49a');
|
||||
});
|
||||
});
|
78
src/lib/server/minecraft.ts
Normal file
78
src/lib/server/minecraft.ts
Normal file
@ -0,0 +1,78 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
export class UserNotFoundError extends Error {
|
||||
constructor(username: string) {
|
||||
super(`Ein Spieler mit dem Namen '${username}' konnte nicht gefunden werden`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getJavaUuid(username: string): Promise<string> {
|
||||
const response = await fetch(`https://api.mojang.com/users/profiles/minecraft/${username}`);
|
||||
if (!response.ok) {
|
||||
throw response.status < 500
|
||||
? new UserNotFoundError(username)
|
||||
: new Error(`mojang server error (${response.status}): ${await response.text()}`);
|
||||
}
|
||||
const json = await response.json();
|
||||
const id: string = json['id'];
|
||||
// prettier-ignore
|
||||
return `${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}`;
|
||||
}
|
||||
|
||||
// https://github.com/carlop3333/XUIDGrabber/blob/main/grabber.js
|
||||
export async function getBedrockUuid(username: string): Promise<string> {
|
||||
const initialPageResponse = await fetch('https://cxkes.me/xbox/xuid');
|
||||
const initialPageContent = await initialPageResponse.text();
|
||||
const token = /name="_token"\svalue="(?<token>\w+)"/.exec(initialPageContent)?.groups?.token;
|
||||
if (token === undefined) throw new Error("couldn't grab token from xuid converter website");
|
||||
|
||||
const cookies = initialPageResponse.headers.get('set-cookie')?.split(' ');
|
||||
if (cookies === undefined)
|
||||
throw new Error("couldn't get response cookies from xuid converter website");
|
||||
else if (cookies.length < 11) throw new Error('xuid converter website sent unexpected cookies');
|
||||
|
||||
const requestBody = new URLSearchParams();
|
||||
requestBody.set('_token', token);
|
||||
requestBody.set('gamertag', username);
|
||||
|
||||
const resultPageResponse = await fetch('https://cxkes.me/xbox/xuid', {
|
||||
method: 'post',
|
||||
body: requestBody,
|
||||
// prettier-ignore
|
||||
headers: {
|
||||
'Host': 'www.cxkes.me',
|
||||
'Accept-Encoding': 'gzip, deflate,br',
|
||||
'Content-Length': Buffer.byteLength(requestBody.toString()).toString(),
|
||||
'Origin': 'https://www.cxkes.me',
|
||||
'DNT': '1',
|
||||
'Connection': 'keep-alive',
|
||||
'Referer': 'https://www.cxkes.me/xbox/xuid',
|
||||
'Cookie': `${cookies[0]} ${cookies[10].slice(0, cookies[10].length - 1)}`,
|
||||
'Upgrade-Insecure-Requests': '1',
|
||||
'Sec-Fectch-Dest': 'document',
|
||||
'Sec-Fetch-Mode': 'navigate',
|
||||
'Sec-Fetch-Site': 'same-origin',
|
||||
'Sec-Fetch-User': '?1',
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0',
|
||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
||||
'Accept-Language': 'es-ES,es;q=0.8,en-US;q=0.5,en;q=0.3',
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
const resultPageContent = await resultPageResponse.text();
|
||||
let xuid: string | undefined;
|
||||
if ((xuid = /id="xuidHex">(?<xuid>\w+)</.exec(resultPageContent)?.groups?.xuid) === undefined) {
|
||||
throw new UserNotFoundError(username);
|
||||
}
|
||||
return `00000000-0000-0000-${xuid.substring(0, 4)}-${xuid.substring(4)}`;
|
||||
}
|
||||
|
||||
// https://gist.github.com/yushijinhun/69f68397c5bb5bee76e80d192295f6e0
|
||||
export function getCrackedUuid(username: string): string {
|
||||
const data = createHash('md5').update(`OfflinePlayer:${username}`).digest('binary').split('');
|
||||
data[6] = String.fromCharCode((data[6].charCodeAt(0) & 0x0f) | 0x30);
|
||||
data[8] = String.fromCharCode((data[8].charCodeAt(0) & 0x3f) | 0x80);
|
||||
const uid = Buffer.from(data.join(''), 'ascii').toString('hex');
|
||||
// prettier-ignore
|
||||
return `${uid.substring(0, 8)}-${uid.substring(8, 12)}-${uid.substring(12, 16)}-${uid.substring(16, 20)}-${uid.substring(20)}`;
|
||||
}
|
5
src/lib/stores.ts
Normal file
5
src/lib/stores.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { type Writable, writable } from 'svelte/store';
|
||||
|
||||
export const registered: Writable<{
|
||||
username: string;
|
||||
} | null> = writable(null);
|
20
src/routes/+layout.svelte
Normal file
20
src/routes/+layout.svelte
Normal file
@ -0,0 +1,20 @@
|
||||
<script>
|
||||
import '../app.css';
|
||||
</script>
|
||||
|
||||
<nav class="navbar fixed top-0 bg-base-100 h-12 z-40">
|
||||
<div class="navbar-start h-full">
|
||||
<a class="h-full" href="/">
|
||||
<img class="rounded h-full" src="/img/craftattack-logo.webp" alt="Logo" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-center flex space-x-20">
|
||||
<a class="link" href="/register">Anmelden</a>
|
||||
<a class="link" href="/rules">Regeln</a>
|
||||
</div>
|
||||
<div class="navbar-end" />
|
||||
</nav>
|
||||
|
||||
<main class="flex min-h-[calc(100vh-64px-16px)] mt-16 mb-4">
|
||||
<slot />
|
||||
</main>
|
12
src/routes/+page.svelte
Normal file
12
src/routes/+page.svelte
Normal file
@ -0,0 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { PUBLIC_START_DATE } from '$env/static/public';
|
||||
import Timer from '$lib/components/Countdown/Countdown.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Craftattack</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="w-full flex justify-center items-center">
|
||||
<Timer end={Date.parse(PUBLIC_START_DATE)} />
|
||||
</div>
|
6
src/routes/+server.ts
Normal file
6
src/routes/+server.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
export const POST = (() => {
|
||||
return json({ scc: true });
|
||||
}) satisfies RequestHandler;
|
3
src/routes/register/+layout.svelte
Normal file
3
src/routes/register/+layout.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="flex justify-center w-full">
|
||||
<slot />
|
||||
</div>
|
23
src/routes/register/+page.svelte
Normal file
23
src/routes/register/+page.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import { fly } from 'svelte/transition';
|
||||
import RegistrationComplete from './RegistrationComplete.svelte';
|
||||
import Register from './Register.svelte';
|
||||
|
||||
let registered = false;
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Craftattack - Anmeldung</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="absolute top-12 grid card w-11/12 xl:w-2/3 2xl:w-1/2 p-6 shadow-lg overflow-hidden">
|
||||
{#if !registered}
|
||||
<div class="col-[1] row-[1]" transition:fly={{ x: -200, duration: 300 }}>
|
||||
<Register on:submit={() => (registered = true)} />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="col-[1] row-[1]" transition:fly={{ x: 200, duration: 300 }}>
|
||||
<RegistrationComplete on:close={() => (registered = false)} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
49
src/routes/register/+server.ts
Normal file
49
src/routes/register/+server.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {
|
||||
getBedrockUuid,
|
||||
getCrackedUuid,
|
||||
getJavaUuid,
|
||||
UserNotFoundError
|
||||
} from '$lib/server/minecraft';
|
||||
import { error, type RequestHandler } from '@sveltejs/kit';
|
||||
import { User } from '$lib/server/database';
|
||||
|
||||
export const POST = (async ({ request }) => {
|
||||
const data = await request.formData();
|
||||
|
||||
let uuid: string;
|
||||
try {
|
||||
// available playertypes are 'java', 'bedrock' and 'cracked'
|
||||
switch (data.get('playertype')) {
|
||||
case 'java':
|
||||
uuid = await getJavaUuid(data.get('username') as string);
|
||||
break;
|
||||
case 'bedrock':
|
||||
uuid = await getBedrockUuid(data.get('username') as string);
|
||||
break;
|
||||
case 'cracked':
|
||||
uuid = getCrackedUuid(data.get('username') as string);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`invalid player type (${data.get('playertype')})`);
|
||||
}
|
||||
} catch (e) {
|
||||
if (e instanceof UserNotFoundError) {
|
||||
throw error(400, e.message);
|
||||
}
|
||||
console.error((e as Error).message);
|
||||
return new Response();
|
||||
}
|
||||
|
||||
await User.create({
|
||||
firstname: data.get('firstname'),
|
||||
lastname: data.get('lastname'),
|
||||
birthday: data.get('birthday'),
|
||||
telephone: data.get('telephone'),
|
||||
username: data.get('username'),
|
||||
playertype: data.get('playertype'),
|
||||
password: data.get('password'),
|
||||
uuid: uuid
|
||||
});
|
||||
|
||||
return new Response();
|
||||
}) satisfies RequestHandler;
|
221
src/routes/register/Register.svelte
Normal file
221
src/routes/register/Register.svelte
Normal file
@ -0,0 +1,221 @@
|
||||
<script lang="ts">
|
||||
import Select from '$lib/components/Input/Select.svelte';
|
||||
import Input from '$lib/components/Input/Input.svelte';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
let checkInputs = () => {};
|
||||
let playertype = 'java';
|
||||
let firstnameInput: HTMLInputElement;
|
||||
let lastnameInput: HTMLInputElement;
|
||||
let birthdayInput: HTMLInputElement;
|
||||
let usernameInput: HTMLInputElement;
|
||||
let privacyInput: HTMLInputElement;
|
||||
let logsInput: HTMLInputElement;
|
||||
let rulesInput: HTMLInputElement;
|
||||
onMount(() => {
|
||||
checkInputs = () => {
|
||||
let allInputs = [
|
||||
firstnameInput,
|
||||
lastnameInput,
|
||||
birthdayInput,
|
||||
usernameInput,
|
||||
privacyInput,
|
||||
logsInput,
|
||||
rulesInput
|
||||
];
|
||||
if (!allInputs.every((v) => v.value || v.checked)) {
|
||||
inputsInvalidMessage = 'Bitte fülle alle erforderlichen Felder aus';
|
||||
} else {
|
||||
inputsInvalidMessage = null;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
async function sendRegister() {
|
||||
// eslint-disable-next-line no-async-promise-executor
|
||||
registerRequest = new Promise(async (resolve, reject) => {
|
||||
const response = await fetch('/register', {
|
||||
method: 'POST',
|
||||
body: new FormData(document.forms[0])
|
||||
});
|
||||
if (response.ok) {
|
||||
dispatch('submit', {});
|
||||
resolve();
|
||||
} else if (response.status < 500) {
|
||||
reject(Error((await response.json()).message));
|
||||
} else {
|
||||
reject(Error(`${response.statusText} (${response.status})`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let inputsInvalidMessage: string | null = 'Bitte fülle alle erforderlichen Felder aus';
|
||||
let registerRequest: Promise<void> | null = null;
|
||||
</script>
|
||||
|
||||
<h1 class="text-center text-3xl lg:text-5xl">Anmeldung</h1>
|
||||
<form id="form" on:input={checkInputs} on:submit|preventDefault={sendRegister}>
|
||||
<div class="divider">Persönliche Angaben</div>
|
||||
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-y-4">
|
||||
<Input
|
||||
id="firstname"
|
||||
name="firstname"
|
||||
type="text"
|
||||
required={true}
|
||||
bind:inputElement={firstnameInput}
|
||||
>
|
||||
<span slot="label">Vorname</span>
|
||||
</Input>
|
||||
<Input
|
||||
id="lastname"
|
||||
name="lastname"
|
||||
type="text"
|
||||
required={true}
|
||||
bind:inputElement={lastnameInput}
|
||||
>
|
||||
<span slot="label">Nachname</span>
|
||||
</Input>
|
||||
<Input
|
||||
id="birthday"
|
||||
name="birthday"
|
||||
type="date"
|
||||
required={true}
|
||||
bind:inputElement={birthdayInput}
|
||||
>
|
||||
<span slot="label">Geburtstag</span>
|
||||
<span slot="notice">Die Angabe hat keine Auswirkungen auf das Spielgeschehen</span>
|
||||
</Input>
|
||||
<Input id="telephone" name="telephone" type="tel">
|
||||
<span slot="label">Telefonnummer</span>
|
||||
<p slot="notice">
|
||||
Diese nutzen wir, um Dich in der Whatsapp-Gruppe zuzuordnen und kontaktieren zu können.
|
||||
<br />
|
||||
<b>Die Angabe ist freiwillig, hilft den Administratoren jedoch sehr!</b>
|
||||
</p>
|
||||
</Input>
|
||||
</div>
|
||||
<div class="divider">Spiel</div>
|
||||
<div class="mx-2 grid grid-cols-1 sm:grid-cols-2 gap-y-4">
|
||||
<Input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required={true}
|
||||
bind:inputElement={usernameInput}
|
||||
>
|
||||
<span slot="label">Minecraft-Spielername</span>
|
||||
</Input>
|
||||
<Select
|
||||
id="playertype"
|
||||
name="playertype"
|
||||
label="Edition"
|
||||
bind:value={playertype}
|
||||
required={true}
|
||||
>
|
||||
<option value="java">Java Edition</option>
|
||||
<option value="bedrock">Bedrock Edition</option>
|
||||
<option value="cracked">Java cracked</option>
|
||||
</Select>
|
||||
{#if playertype === 'cracked'}
|
||||
<div class="sm:col-span-2">
|
||||
<Input id="password" name="password" type="password" required={true}>
|
||||
<span slot="label">Passwort</span>
|
||||
<span slot="notice">
|
||||
Da Du cracked spielst, musst Du ein Passwort festlegen, mit welchem Du Dich auf dem
|
||||
Server authentifizierst! Das Passwort wird im Klartext gespeichert und ist in deinen
|
||||
Clientlogs sowie in Serverlogs für Admins sichtbar. Verwende daher ein neues Passwort,
|
||||
welches Du nirgends sonst verwendest! Merke Dir das Passwort gut, ohne kannst Du Dich
|
||||
nicht auf dem Server einloggen
|
||||
</span>
|
||||
</Input>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="divider" />
|
||||
<div class="mx-2 grid gap-y-3 mb-6">
|
||||
<div class="flex gap-4">
|
||||
<Input
|
||||
id="privacy"
|
||||
name="privacy"
|
||||
type="checkbox"
|
||||
required={true}
|
||||
bind:inputElement={privacyInput}
|
||||
/>
|
||||
<label for="privacy">
|
||||
<span>
|
||||
Ich bin mit der Speicherung meiner in der Anmeldung angegebenen, persönlichen Daten
|
||||
einverstanden. Siehe <a class="link" href="https://mhsl.eu/id.html">Datenschutz</a>
|
||||
</span>
|
||||
<span class="text-red-700">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Input id="logs" name="logs" type="checkbox" required={true} bind:inputElement={logsInput} />
|
||||
<label for="logs">
|
||||
<span>
|
||||
Ich bin mit der Speicherung in Form von Logs aller meiner, beim Spielen anfallenden,
|
||||
persönlichen Daten durch den Server einverstanden
|
||||
</span>
|
||||
<span class="text-red-700">*</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex gap-4">
|
||||
<Input
|
||||
id="rules"
|
||||
name="rules"
|
||||
type="checkbox"
|
||||
required={true}
|
||||
bind:inputElement={rulesInput}
|
||||
/>
|
||||
<label for="rules">
|
||||
Ich bin mit den <a class="link" href="/rules">Regeln</a> einverstanden und achte sie
|
||||
<span class="text-red-700">*</span>
|
||||
<br />
|
||||
<p class="text-[.75rem]">
|
||||
Dies betrifft jede Interaktion im Spiel und zugehörige Daten wie z.B. Chatnachrichten
|
||||
welche vom Minecraft Client an den Server übermittelt werden
|
||||
</p>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class={inputsInvalidMessage !== null ? 'tooltip tooltip-top' : 'grid w-min'}
|
||||
data-tip={inputsInvalidMessage}
|
||||
>
|
||||
<div class="row-[1] col-[1]">
|
||||
<Input
|
||||
id="submit"
|
||||
type="submit"
|
||||
value="Anmeldung absenden"
|
||||
disabled={inputsInvalidMessage !== null || registerRequest !== null}
|
||||
/>
|
||||
</div>
|
||||
{#key registerRequest}
|
||||
{#if registerRequest}
|
||||
{#await registerRequest}
|
||||
<span
|
||||
class="relative top-[calc(50%-12px)] left-[calc(50%-12px)] row-[1] col-[1] loading loading-ring"
|
||||
/>
|
||||
{:catch error}
|
||||
<dialog
|
||||
class="modal"
|
||||
on:close={() => setTimeout(() => (registerRequest = null), 200)}
|
||||
open
|
||||
>
|
||||
<form method="dialog" class="modal-box">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
<h3 class="font-bold text-lg">Error</h3>
|
||||
<p class="py-4">{error.message}</p>
|
||||
</form>
|
||||
<form method="dialog" class="modal-backdrop bg-[rgba(0,0,0,.2)]">
|
||||
<button>close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
{/await}
|
||||
{/if}
|
||||
{/key}
|
||||
</div>
|
||||
</form>
|
32
src/routes/register/RegistrationComplete.svelte
Normal file
32
src/routes/register/RegistrationComplete.svelte
Normal file
@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import { IconSolid } from 'svelte-heros-v2';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { PUBLIC_START_DATE } from '$env/static/public';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let startDayOptions: Intl.DateTimeFormatOptions = {
|
||||
day: '2-digit',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
};
|
||||
let startTimeOptions: Intl.DateTimeFormatOptions = {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="flex items-center h-12 mb-2">
|
||||
<button class="sm:absolute btn btn-sm btn-square" on:click={() => dispatch('close')}>
|
||||
<IconSolid name="chevron-left-solid" />
|
||||
</button>
|
||||
<h1 class="text-center text-xl sm:text-3xl m-auto">Registrierung erfolgreich</h1>
|
||||
</div>
|
||||
<p>
|
||||
<b>Du hast dich erfolgreich für Craftattack 6 registriert</b>. Spielstart ist am {new Date(
|
||||
PUBLIC_START_DATE
|
||||
).toLocaleString('de-DE', startDayOptions)} um {new Date(PUBLIC_START_DATE).toLocaleString(
|
||||
'de-DE',
|
||||
startTimeOptions
|
||||
)} Uhr.
|
||||
</p>
|
3
src/routes/rules/+layout.svelte
Normal file
3
src/routes/rules/+layout.svelte
Normal file
@ -0,0 +1,3 @@
|
||||
<div class="mx-4 sm:mx-48">
|
||||
<slot />
|
||||
</div>
|
121
src/routes/rules/+page.svelte
Normal file
121
src/routes/rules/+page.svelte
Normal file
@ -0,0 +1,121 @@
|
||||
<svelte:head>
|
||||
<title>Craftattack - Regeln</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1 class="text-3xl lg:text-5xl mb-4">CraftAttack 5 Regelwerk</h1>
|
||||
<p>
|
||||
Das Lesen der Regeln ist für alle Teilnehmer verpflichtend. Die Regeln sollen für einen
|
||||
reibungslosen und strukturierte Ablauf des Projekts sorgen, weshalb das Lesen der Regeln ein
|
||||
essenzieller Bestandteil für das Gelingen von CraftAttack 5 ist. Die Regeln sind wörtlich zu
|
||||
verstehen und sind Grundlage für das Projekt. Zur Vereinfachung gehen sie nicht zu weit ins Detail
|
||||
und deuten teils nur umfangreiche Themengebiete an. Entscheidungen werden, wenn von Spielern
|
||||
angeregt, dann durch die Administratoren getroffen, die sich an den Regeln orientieren.
|
||||
</p>
|
||||
<ol class="p-[revert] list-decimal my-6">
|
||||
<li>
|
||||
Oberste Priorität hat der respektvolle und tolerante Umgang der Spieler untereinander. Der
|
||||
Spielspaß, der offene Umgang miteinander und die Interaktion aller steht im Vordergrund, weshalb
|
||||
Drohungen, Belästigungen oder sonstige gegenüber anderen Spielern respektlose Aktivitäten
|
||||
strengstens verboten sind und auch hart geahndet werden.
|
||||
</li>
|
||||
<li>
|
||||
Selbstverständlich sind sämtliche Inhalte (Minecraft-Namen, Skins, Chat-Nachrichten, Links,
|
||||
etc.) mit sexistischen, diskriminierenden, rassistischen, pornographischen oder illegalen
|
||||
Inhalten nicht erlaubt. Außerdem ist es nicht gestattet, den Chat mit Nachrichten jeglicher Art
|
||||
vollzuspammen. Des Weiteren sollte der MC-Name des Spielers, der bei der Anmeldung angegeben
|
||||
wird, bis zum Ende des Projekts nicht geändert werden. Das Nutzen bzw. Anmelden von
|
||||
Zweitaccounts ist nicht gestattet.
|
||||
</li>
|
||||
<li>
|
||||
Jegliche Clientmodifications, die deutliche Vorteile gegenüber anderen Spielern erbringen, sind
|
||||
nicht gestattet. Alle Spieler, die kein Minecraft Vanilla spielen, sind verpflichtet ihre
|
||||
Clients oder Modifications dahingehend zu überprüfen, ob durch diese entscheidende Vorteile
|
||||
erlangt werden können. Solche Modifications sind zu entfernen. Das Nutzen von simplen Mini-Maps
|
||||
(Draufsicht) oder Cosmetics ist selbstverständlich erlaubt. Es liegt im Allgemeinen im Interesse
|
||||
der Administratoren, dass Spieler es nicht übertreiben und sich keine Vorteile schaffen, die
|
||||
gegenüber anderen ungerecht sind. Dabei setzen die Administratoren auch auf eine Selbstreflexion
|
||||
jedes einzelnen. Im Zweifel sind Spieler dazu angehalten, einen Administrator zu kontaktieren,
|
||||
der genauer Informationen übermitteln kann.
|
||||
</li>
|
||||
<li>
|
||||
Das Erbauen und Betreiben lag-erzeugender Maschinen, Farmen (Zero-Tick-Farmen etc.) oder andere
|
||||
Bauten, die den Spielfluss stören könnten, ist verboten. Im Zweifelsfall ist eine Anfrage bei
|
||||
den Administratoren erwünscht. Bei beispielsweise Farmen oder allgemeinen Redstone Schaltung
|
||||
sollten diese auch nur dann aktiviert werden, wenn nötig. Außerdem sollten überdimensional große
|
||||
Villager-Baukomplexe nur in Absprache mit Administratoren errichtet und betreiben werden.
|
||||
Selbstverständlich ist das Erbauen von Farmen ein essenzieller Bestandteil des Projekts und
|
||||
stellt auch kein Problem dar, solange die oben genannten Bedingungen eingehalten werden.
|
||||
</li>
|
||||
<li>
|
||||
Das Verkaufen von Items ist allgemein jedem Spieler überall gestattet. Jedoch bietet es sich an
|
||||
und ist wünschenswert, die Shops aller Spieler in einem Shoppingdistrict gemeinsam anzusiedeln,
|
||||
um die Interaktion zu fördern. Ein Shop muss sich innerhalb des ausgewiesenen Bereiches befinden
|
||||
und muss ebenso über das dort bestehende Wegenetz erreichbar sein. Der Shoppingdistrict ist
|
||||
ausschließlich zum Bauen von Shops vorgesehen. Mehrere Shops zu einem bestimmten Item sind
|
||||
möglich und auch erwünscht. Diebstahl im Shoppingdistrict untersteht denselben Strafen wie
|
||||
allgemeiner Diebstahl. Ein angemessener Abstand der privaten Strukturen vom Shoppingdisrict ist
|
||||
einzuhalten.
|
||||
</li>
|
||||
<li>
|
||||
Das Abstecken bestimmter Gebiete ist grundsätzlich erlaubt, jedoch sind unangemessen große
|
||||
Grundstücke untersagt. Das maximale Maß ist im Einzelfall zu entscheiden. Die Grenzen bereits
|
||||
abgesteckter Grundstücke sind unveränderlich. Im Fall von Inaktivität oder andere Beschwerden
|
||||
beziehungsweise Verstöße ist ein Administrator zu kontaktieren.
|
||||
</li>
|
||||
<li>
|
||||
Wie bereits angedeutet, ist das Ziel ein Umgang untereinander, der für keinen negative Aspekte
|
||||
beinhaltet. So ist es beispielsweise nicht gestattet, andere zu bestehlen oder ohne Erlaubnis
|
||||
Bauwerke anderer zu verändern. Dabei liegt natürlich eine gewisse Toleranzbereitschaft vor und
|
||||
ein Spielraum, der allerdings nicht überschritten werden darf. Dies gilt sowohl für das
|
||||
Bestehlen anderer als auch für Griefing (zerstören von Bauten anderer ohne Erlaubnis etc.).
|
||||
Außerdem ist das Töten anderer ohne nachvollziehbaren Grund verboten. Natürlich ist auch hier
|
||||
eine Bewertung jeder einzelnen Situation notwendig, weshalb eine Verallgemeinerung hier bewusst
|
||||
nicht angeführt wird. Fest steht jedoch, dass klar zwischen einem Töten aus Spaß mit geringen
|
||||
Folgen und einem mehrmaligen - ja sogar permanenten Töten anderer mit schlimmeren Folgen,
|
||||
unterschieden wird.
|
||||
</li>
|
||||
<li>
|
||||
Allgemein liegt es in der Hand der Administratoren einzelne Situation zu bewerten, Strafen zu
|
||||
verhängen und Entscheidungen zu treffen. Ein internes Strikesystem der Administratoren sorgt für
|
||||
eine Gleichberechtigung aller Spieler. Des Weiteren erfolgt eine Absprache unter den
|
||||
Administratoren, um alle Sichtweisen miteinzubringen. Wichtig ist zusätzlich, dass alle
|
||||
Entscheidungen der Administratoren im Sinne des Projekts getroffen werden. Den Entscheidungen
|
||||
und Anweisungen der Administratoren ist stets Folge zu leisten, wenn diese als Administrator
|
||||
fungieren. Im normalen Spielbetrieb sind sie normale Mitspieler ohne spielentscheidende
|
||||
Sonderrechte. So ist es nicht ihre Aufgabe überall nach dem Rechten zu sehen, sondern
|
||||
Ansprechpartner zu sein, um dann nach der Vorlegung eines Problems durch einen Geschädigten die
|
||||
Administratorenrolle einzunehmen und dementsprechend zu handeln. In dem Feld ist einzutragen,
|
||||
wobei die Regeln trotzdem bis zum Ende gelesen werden müssen. Allgemein gilt immer der
|
||||
Grundsatz, dass ein Eingriff der Administratoren nur dann erfolgt, wenn dies die Spieler auch
|
||||
fordern. Solange beide Parteien zufrieden sind, passiert natürlich auch nichts. Wenn also
|
||||
beispielsweise zwei Spieler ein bewusstes pvp-Duell starten, zieht das logischer Weise keine
|
||||
Konsequenzen nach sich.
|
||||
</li>
|
||||
<li>
|
||||
Jedem Teilnehmer ist es möglich sich an den Support/das Administratoren-Team zu wenden. Zu den
|
||||
Administratoren gehören die Spieler, die auf dem Server mit einem Admin-Tag versehen sind. Zwei
|
||||
von diesen sind außerdem Administrator der WhatsApp-Gruppe. Eine Kontaktaufnahme ist direkt auf
|
||||
dem Server im Chat oder auf dem Teamspeak: „mhsl.eu“ möglich. Außerdem können sie über WhatsApp
|
||||
angeschrieben werden, wenn sich z.B. gerade kein Administrator auf dem Server befindet oder bei
|
||||
anderen Rückfragen. Bei Unzufriedenheit, Meldung eines Regelverstoßen, Anregungen oder Fragen
|
||||
steht das Administratoren-Team allen Spielern jederzeit zu Verfügung.
|
||||
</li>
|
||||
<li>
|
||||
Konflikte sollen grundlegend zuerst auf einer Ebene zwischen den Spielern geschlichtet werden,
|
||||
bevor ein Administrator kontaktiert wird. Jeder Regelverstoß zieht unterschiedliche Folgen nach
|
||||
sich, die von Ermahnungen, über Tagesbänne bis zum permanenten Bann führen können. Diese
|
||||
möglichen Konsequenzen sind von allen Teilnehmern zu akzeptieren.
|
||||
</li>
|
||||
</ol>
|
||||
<p>
|
||||
Alle aufgeführten Regeln und die damit in Verbindung stehende Angaben erfolgen ohne Gewähr auf
|
||||
Vollständigkeit, Richtigkeit und Aktualität. Das Durchsetzen der Regeln liegt im Ermessen der
|
||||
Administratoren, die vorher in Absprache mit dem Geschädigten eine der Situation angemessene
|
||||
Maßnahmen getroffen haben.
|
||||
</p>
|
||||
|
||||
<style lang="postcss">
|
||||
li {
|
||||
@apply mb-2;
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user